Files

452 lines
16 KiB
JavaScript

/* Processes page — sovereign://processes
*
* Tabs: Processes | Tabs
*
* - Processes: /proc-level view of all sovereign_browser PIDs (main,
* WebKit renderers with their hosted tabs, WebKit network/gpu, Tor,
* FIPS). Polled from sovereign://processes/list every 1s. Sortable
* columns. Renderer rows expand to show hosted tabs.
* - Tabs: per-tab probe view (cpu_busy_percent, fps, timers, net, heap,
* DOM, WPID). Polled from sovereign://processes/tabs every 1s.
* Click a row to load the drill-down (sovereign://processes/tab_probe).
*
* Uses XMLHttpRequest via sovereignGet() because WebKit's custom scheme
* handler does not reliably support fetch() for sovereign:// URLs.
*/
function sovereignGet(url) {
return new Promise(function (resolve, reject) {
var x = new XMLHttpRequest();
x.open('GET', url, true);
x.onreadystatechange = function () {
if (x.readyState !== 4) return;
try { resolve(JSON.parse(x.responseText)); }
catch (e) { reject(e); }
};
x.onerror = function () { reject(new Error('XHR error')); };
x.send();
});
}
function esc(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function showMsg(text, isErr) {
var el = document.getElementById('status-msg');
el.textContent = text || '';
el.className = 'status show ' + (isErr ? 'err' : 'ok');
if (!text) el.className = 'status';
}
/* ── Formatting helpers ─────────────────────────────────────────── */
function fmtKb(kb) {
if (kb == null || kb < 0) return '—';
if (kb >= 1024 * 1024) return (kb / (1024 * 1024)).toFixed(1) + 'G';
if (kb >= 1024) return (kb / 1024).toFixed(0) + 'M';
return kb + 'K';
}
function fmtUptime(sec) {
if (sec == null || sec < 0) return '—';
var d = Math.floor(sec / 86400);
var h = Math.floor((sec % 86400) / 3600);
var m = Math.floor((sec % 3600) / 60);
var s = Math.floor(sec % 60);
if (d > 0) return d + 'd' + h + 'h';
if (h > 0) return h + 'h' + m + 'm';
if (m > 0) return m + 'm' + s + 's';
return s + 's';
}
function heatClass(pct) {
if (pct == null) return '';
if (pct >= 90) return 'hot';
return '';
}
function heatHtml(pct) {
if (pct == null) return '<span class="heat">—</span>';
return '<span class="heat ' + heatClass(pct) + '">' +
pct.toFixed(1) + '</span>';
}
/* ── Tab switching ──────────────────────────────────────────────── */
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
document.querySelectorAll('.tab-panel').forEach(function (panel) {
panel.classList.toggle('active', panel.id === 'tab-' + tabName);
});
}
document.querySelectorAll('.tab-btn').forEach(function (btn) {
btn.addEventListener('click', function () { switchTab(btn.dataset.tab); });
});
/* ── Processes tab ──────────────────────────────────────────────── */
var gProcSortKey = 'cpu_percent';
var gProcSortDir = 'desc';
var gProcData = [];
var gExpandedRenderers = {}; // pid -> bool
function setProcSort(key) {
if (gProcSortKey === key) {
gProcSortDir = gProcSortDir === 'asc' ? 'desc' : 'asc';
} else {
gProcSortKey = key;
gProcSortDir = (key === 'name' || key === 'state' || key === 'ownership')
? 'asc' : 'desc';
}
renderProcTable();
}
document.querySelectorAll('#proc-table th[data-sort]').forEach(function (th) {
th.addEventListener('click', function () { setProcSort(th.dataset.sort); });
});
function procSorter(a, b) {
var ka = a[gProcSortKey];
var kb = b[gProcSortKey];
var cmp;
if (typeof ka === 'number' || typeof kb === 'number') {
cmp = (ka || 0) - (kb || 0);
} else {
cmp = String(ka || '').localeCompare(String(kb || ''));
}
return gProcSortDir === 'asc' ? cmp : -cmp;
}
function renderProcTable() {
var body = document.getElementById('proc-body');
body.innerHTML = '';
var sorted = gProcData.slice().sort(procSorter);
// Update sort indicators.
document.querySelectorAll('#proc-table th[data-sort]').forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
if (th.dataset.sort === gProcSortKey) {
th.classList.add(gProcSortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
}
});
sorted.forEach(function (p) {
var tr = document.createElement('tr');
var isRenderer = p.ownership === 'webkit-renderer';
tr.className = isRenderer ? 'renderer-row' : '';
var tabsCount = (p.hosted_tabs || []).length;
var tabsCell;
if (isRenderer) {
tabsCell = String(tabsCount);
} else {
tabsCell = '—';
}
tr.innerHTML =
'<td>' + p.pid + '</td>' +
'<td class="title-cell">' + esc(p.name) + '</td>' +
'<td>' + heatHtml(p.cpu_percent) + '</td>' +
'<td>' + fmtKb(p.rss_kb) + '</td>' +
'<td>' + fmtKb(p.pss_kb) + '</td>' +
'<td>' + (p.threads || 0) + '</td>' +
'<td>' + fmtUptime(p.uptime_sec) + '</td>' +
'<td>' + esc(p.state) + '</td>' +
'<td>' + esc(p.ownership) +
(p.service_state ? ' (' + esc(p.service_state) + ')' : '') + '</td>' +
'<td>' + tabsCell + '</td>';
if (isRenderer) {
(function (pid, hosted) {
tr.addEventListener('click', function () {
gExpandedRenderers[pid] = !gExpandedRenderers[pid];
renderProcTable();
});
})(p.pid, p.hosted_tabs);
}
body.appendChild(tr);
// Expanded hosted-tabs row.
if (isRenderer && gExpandedRenderers[p.pid] && tabsCount > 0) {
var sub = document.createElement('tr');
sub.className = 'hosted-tabs-row';
var html = '<td colspan="10">';
p.hosted_tabs.forEach(function (t) {
html += '<span class="ht-entry"><span class="ht-idx">#' +
t.index + '</span>' + esc(t.title || '(untitled)') +
' <span class="url-sub">' + esc(t.url) + '</span></span>';
});
html += '</td>';
sub.innerHTML = html;
body.appendChild(sub);
}
});
if (sorted.length === 0) {
body.innerHTML = '<tr><td colspan="10" class="empty">No processes.</td></tr>';
}
}
async function fetchProcesses() {
try {
var data = await sovereignGet('sovereign://processes/list?_t=' + Date.now());
gProcData = Array.isArray(data) ? data : [];
renderProcTable();
} catch (e) {
showMsg('Failed to load processes: ' + e.message, true);
}
}
/* ── Tabs tab ───────────────────────────────────────────────────── */
var gTabsSortKey = 'cpu_busy_percent';
var gTabsSortDir = 'desc';
var gTabsData = [];
var gSelectedTab = -1;
function setTabsSort(key) {
if (gTabsSortKey === key) {
gTabsSortDir = gTabsSortDir === 'asc' ? 'desc' : 'asc';
} else {
gTabsSortKey = key;
gTabsSortDir = (key === 'title') ? 'asc' : 'desc';
}
renderTabsTable();
}
document.querySelectorAll('#tabs-table th[data-sort]').forEach(function (th) {
th.addEventListener('click', function () { setTabsSort(th.dataset.sort); });
});
function tabSorter(a, b) {
var ka = a[gTabsSortKey];
var kb = b[gTabsSortKey];
// For probe fields, fall back to -1 if probe is null.
if (gTabsSortKey !== 'index' && gTabsSortKey !== 'title' &&
gTabsSortKey !== 'webprocess_pid') {
ka = (a.probe && a.probe[gTabsSortKey] != null) ? a.probe[gTabsSortKey] : -1;
kb = (b.probe && b.probe[gTabsSortKey] != null) ? b.probe[gTabsSortKey] : -1;
}
var cmp;
if (typeof ka === 'number' || typeof kb === 'number') {
cmp = (ka || 0) - (kb || 0);
} else {
cmp = String(ka || '').localeCompare(String(kb || ''));
}
return gTabsSortDir === 'asc' ? cmp : -cmp;
}
function probeField(tab, key, fmt) {
if (!tab.probe) return '—';
var v = tab.probe[key];
if (v == null || v < 0) return '—';
return fmt ? fmt(v) : v;
}
function renderTabsTable() {
var body = document.getElementById('tabs-body');
body.innerHTML = '';
var sorted = gTabsData.slice().sort(tabSorter);
document.querySelectorAll('#tabs-table th[data-sort]').forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
if (th.dataset.sort === gTabsSortKey) {
th.classList.add(gTabsSortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
}
});
sorted.forEach(function (t) {
var tr = document.createElement('tr');
tr.className = 'tab-row' + (t.index === gSelectedTab ? ' selected' : '');
var busy = (t.probe && t.probe.cpu_busy_percent != null)
? t.probe.cpu_busy_percent : null;
var titleClass = (t.index === gSelectedTab) ? '' : 'dim';
tr.innerHTML =
'<td>' + t.index + '</td>' +
'<td class="title-cell"><span class="' + titleClass + '">' +
esc(t.title || '(untitled)') + '</span>' +
(t.is_internal ? ' (internal)' : '') +
'<br><span class="url-sub">' + esc(t.url) + '</span></td>' +
'<td>' + (t.is_internal ? '<span class="heat">—</span>'
: heatHtml(busy)) + '</td>' +
'<td>' + probeField(t, 'fps') + '</td>' +
'<td>' + probeField(t, 'timer_count') + '</td>' +
'<td>' + probeField(t, 'net_in_flight') + '</td>' +
'<td>' + probeField(t, 'heap_used_mb', function (v) { return v + 'M'; }) + '</td>' +
'<td>' + probeField(t, 'dom_node_count') + '</td>' +
'<td>' + (t.webprocess_pid || 0) + '</td>';
(function (idx) {
tr.addEventListener('click', function () { openDrilldown(idx); });
})(t.index);
body.appendChild(tr);
});
if (sorted.length === 0) {
body.innerHTML = '<tr><td colspan="9" class="empty">No tabs.</td></tr>';
}
}
async function fetchTabs() {
try {
var data = await sovereignGet('sovereign://processes/tabs?_t=' + Date.now());
gTabsData = Array.isArray(data) ? data : [];
renderTabsTable();
// If a tab is selected, refresh its drill-down too.
if (gSelectedTab >= 0) refreshDrilldown();
} catch (e) {
showMsg('Failed to load tabs: ' + e.message, true);
}
}
/* ── Drill-down ─────────────────────────────────────────────────── */
async function openDrilldown(idx) {
gSelectedTab = idx;
renderTabsTable();
document.getElementById('drilldown').style.display = 'block';
await refreshDrilldown();
}
async function refreshDrilldown() {
if (gSelectedTab < 0) return;
try {
var d = await sovereignGet('sovereign://processes/tab_probe?index=' +
gSelectedTab);
renderDrilldown(d);
} catch (e) {
document.getElementById('dd-title').textContent = 'Tab #' + gSelectedTab;
document.getElementById('dd-summary').innerHTML =
'<p class="err-text">Failed to load drill-down: ' + esc(e.message) + '</p>';
}
}
function renderDrilldown(d) {
document.getElementById('dd-title').textContent =
'Tab #' + d.index + ' — ' + (d.title || '(untitled)');
var p = d.probe || {};
var rows = [
['URL', esc(d.url)],
['WebProcess PID', d.webprocess_pid || 0],
['CPU-busy %', p.cpu_busy_percent != null ? heatHtml(p.cpu_busy_percent) : '—'],
['FPS', p.fps != null ? p.fps : '—'],
['Active timers', p.timer_count != null ? p.timer_count : '—'],
['Net in-flight', p.net_in_flight != null ? p.net_in_flight : '—'],
['Net reqs (last sec)', p.net_requests_last_sec != null ? p.net_requests_last_sec : '—'],
['Heap used', p.heap_used_mb >= 0 ? p.heap_used_mb + 'M' : '—'],
['Event listeners', p.event_listener_count != null ? p.event_listener_count : '—'],
['Workers', p.worker_count != null ? p.worker_count : '—'],
['DOM nodes', p.dom_node_count >= 0 ? p.dom_node_count : '—']
];
var html = '';
rows.forEach(function (r) {
html += '<div class="info-row"><span>' + r[0] + '</span><span>' +
r[1] + '</span></div>';
});
document.getElementById('dd-summary').innerHTML = html;
// Long-task sources.
var ltBody = document.getElementById('dd-lt-sources-body');
ltBody.innerHTML = '';
(p.long_task_top_sources || []).forEach(function (s) {
ltBody.innerHTML += '<tr><td class="mono">' + esc(s.name) + '</td><td>' +
s.total_ms + '</td><td>' + s.count + '</td></tr>';
});
if (!p.long_task_top_sources || p.long_task_top_sources.length === 0) {
ltBody.innerHTML = '<tr><td colspan="3" class="empty">No long tasks.</td></tr>';
}
// Timers.
var tmBody = document.getElementById('dd-timers-body');
tmBody.innerHTML = '';
(p.timer_top || []).forEach(function (s) {
tmBody.innerHTML += '<tr><td class="mono">' + esc(s.code) + '</td><td>' +
s.interval_ms + '</td><td>' + s.count + '</td></tr>';
});
if (!p.timer_top || p.timer_top.length === 0) {
tmBody.innerHTML = '<tr><td colspan="3" class="empty">No active timers.</td></tr>';
}
// Network endpoints.
var netBody = document.getElementById('dd-net-body');
netBody.innerHTML = '';
(p.net_top_endpoints || []).forEach(function (s) {
netBody.innerHTML += '<tr><td class="mono">' + esc(s.url) + '</td><td>' +
s.count + '</td></tr>';
});
if (!p.net_top_endpoints || p.net_top_endpoints.length === 0) {
netBody.innerHTML = '<tr><td colspan="2" class="empty">No network requests.</td></tr>';
}
// Timeline.
var tl = document.getElementById('dd-timeline');
tl.innerHTML = '';
var timeline = p.long_task_timeline || [];
if (timeline.length === 0) {
tl.innerHTML = '<div class="tl-empty">No long tasks in the last 30s.</div>';
} else {
var maxDur = 1;
timeline.forEach(function (r) { if (r.duration > maxDur) maxDur = r.duration; });
timeline.forEach(function (r) {
var h = Math.max(2, Math.round((r.duration / maxDur) * 72));
var bar = document.createElement('span');
bar.className = 'tl-bar';
bar.style.height = h + 'px';
bar.title = r.duration + 'ms — ' + (r.name || 'unknown');
tl.appendChild(bar);
});
}
}
/* ── Drill-down actions ─────────────────────────────────────────── */
document.getElementById('dd-reload').addEventListener('click', function () {
if (gSelectedTab < 0) return;
// Trigger reload via the bridge — there's no dedicated endpoint, so
// we use the agent eval path indirectly. Simplest: navigate the tab
// by opening its URL again via sovereign://processes/tab_action.
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
'&action=reload').then(function () { refreshDrilldown(); });
});
document.getElementById('dd-suspend').addEventListener('click', function () {
if (gSelectedTab < 0) return;
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
'&action=suspend').then(function () { refreshDrilldown(); });
});
document.getElementById('dd-close').addEventListener('click', function () {
if (gSelectedTab < 0) return;
if (!confirm('Close tab #' + gSelectedTab + '?')) return;
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
'&action=close').then(function () {
document.getElementById('drilldown').style.display = 'none';
gSelectedTab = -1;
fetchTabs();
});
});
document.getElementById('dd-close-panel').addEventListener('click', function () {
document.getElementById('drilldown').style.display = 'none';
gSelectedTab = -1;
renderTabsTable();
});
/* ── Refresh button + polling ───────────────────────────────────── */
document.getElementById('refresh-btn').addEventListener('click', function () {
fetchProcesses();
fetchTabs();
});
function poll() {
fetchProcesses();
fetchTabs();
}
poll();
setInterval(poll, 1000);