import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
const clientId = window.location.pathname.split("/")[1];
let ws = null;
let processes = [];
let processMap = new Map();
let processTree = [];
let collapsedPids = new Set();
let selectedPid = null;
let sortField = "cpu";
let sortDirection = "desc";
let searchTerm = "";
const statusEl = document.getElementById("status-indicator");
const processCountEl = document.getElementById("process-count");
const processListEl = document.getElementById("process-list");
const refreshBtn = document.getElementById("refresh-btn");
const killBtn = document.getElementById("kill-btn");
const searchInput = document.getElementById("search-input");
const clientIdHeader = document.getElementById("client-id-header");
if (clientIdHeader) {
clientIdHeader.textContent = `${clientId} - Process Manager`;
}
function connect() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/api/clients/${clientId}/processes/ws`;
ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
console.log("Process manager connected");
updateStatus("connected", "Connected");
enableControls(true);
requestProcessList();
};
ws.onmessage = (event) => {
const msg = decodeMsgpack(event.data);
if (!msg) {
console.error("Failed to decode message");
return;
}
handleMessage(msg);
};
ws.onerror = (err) => {
console.error("WebSocket error:", err);
updateStatus("error", "Connection Error");
};
ws.onclose = () => {
console.log("Process manager disconnected");
updateStatus("disconnected", "Disconnected");
enableControls(false);
setTimeout(() => connect(), 3000);
};
}
function updateStatus(state, text) {
const icons = {
connecting: '',
connected: '',
error: '',
disconnected: '',
};
statusEl.innerHTML = `${icons[state] || icons.disconnected} ${text}`;
statusEl.className =
state === "connected"
? "inline-flex items-center gap-2 px-3 py-2 rounded-full bg-green-900/40 text-green-100 border border-green-700/60"
: "inline-flex items-center gap-2 px-3 py-2 rounded-full bg-slate-800 text-slate-300";
}
function enableControls(enabled) {
refreshBtn.disabled = !enabled;
updateKillButton();
}
function updateKillButton() {
killBtn.disabled = !selectedPid || !ws || ws.readyState !== WebSocket.OPEN;
}
function send(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(encodeMsgpack(msg));
}
}
function handleMessage(msg) {
console.log("Received:", msg.type);
switch (msg.type) {
case "ready":
console.log("Session ready:", msg.sessionId);
break;
case "status":
if (msg.status === "offline") {
updateStatus("error", "Client Offline");
enableControls(false);
}
break;
case "process_list_result":
handleProcessList(msg);
break;
case "command_result":
handleCommandResult(msg);
break;
default:
console.log("Unknown message type:", msg.type);
}
}
function requestProcessList() {
send({ type: "process_list" });
updateStatus("connected", "Loading processes...");
}
function handleProcessList(msg) {
if (msg.error) {
processListEl.innerHTML = `
${escapeHtml(msg.error)}
`;
updateStatus("error", "Error loading processes");
return;
}
processes = (msg.processes || []).map((proc) => {
const normalizeId = (value) => {
if (typeof value === "bigint") {
return Number(value);
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
return Number.isFinite(value) ? value : 0;
};
return {
...proc,
pid: normalizeId(proc.pid),
ppid: normalizeId(proc.ppid),
};
});
processCountEl.innerHTML = ` ${processes.length} processes`;
updateStatus("connected", "Connected");
buildProcessTree();
renderProcesses();
}
function buildProcessTree() {
processMap.clear();
processes.forEach((proc) => {
processMap.set(proc.pid, { ...proc, children: [] });
});
const roots = [];
processMap.forEach((proc) => {
if (proc.ppid && processMap.has(proc.ppid)) {
processMap.get(proc.ppid).children.push(proc);
} else {
roots.push(proc);
}
});
function sortChildren(proc) {
if (proc.children.length > 0) {
proc.children.sort((a, b) => {
let aVal = a[sortField];
let bVal = b[sortField];
if (sortField === "name") {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (sortDirection === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
proc.children.forEach((child) => sortChildren(child));
}
}
roots.forEach((proc) => sortChildren(proc));
roots.sort((a, b) => {
let aVal = a[sortField];
let bVal = b[sortField];
if (sortField === "name") {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (sortDirection === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
processTree = roots;
}
function renderProcesses() {
const filtered = [];
function collectMatches(proc, depth = 0) {
const matches =
!searchTerm ||
proc.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
proc.pid.toString().includes(searchTerm) ||
(proc.username &&
proc.username.toLowerCase().includes(searchTerm.toLowerCase()));
if (matches) {
filtered.push({ ...proc, depth });
}
if (
proc.children &&
proc.children.length > 0 &&
!collapsedPids.has(proc.pid)
) {
proc.children.forEach((child) => collectMatches(child, depth + 1));
}
}
processTree.forEach((proc) => collectMatches(proc, 0));
if (filtered.length === 0) {
processListEl.innerHTML =
'No processes found
';
return;
}
processListEl.innerHTML = "";
filtered.forEach((proc) => {
const row = createProcessRow(proc, proc.depth);
processListEl.appendChild(row);
});
}
function createProcessRow(proc, depth = 0) {
const row = document.createElement("div");
row.className =
"process-row grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors";
row.dataset.pid = proc.pid;
if (selectedPid === proc.pid) {
row.classList.add("selected");
}
const cpuColor =
proc.cpu > 50
? "text-red-400"
: proc.cpu > 20
? "text-yellow-400"
: "text-slate-400";
const memoryStr = formatBytes(proc.memory);
const hasChildren = proc.children && proc.children.length > 0;
const isCollapsed = collapsedPids.has(proc.pid);
const indent = " ".repeat(depth);
let treeIcon = "";
if (hasChildren) {
treeIcon = `${isCollapsed ? "▶" : "▼"}`;
} else if (depth > 0) {
treeIcon = '';
}
let nameColor = "text-slate-200";
let iconColor = "text-blue-400";
if (proc.type === "system") {
nameColor = "text-purple-400";
iconColor = "text-purple-400";
} else if (proc.type === "service") {
nameColor = "text-cyan-400";
iconColor = "text-cyan-400";
} else if (proc.type === "own") {
nameColor = "text-green-300";
iconColor = "text-green-400";
}
row.innerHTML = `
${proc.pid}
${indent}${treeIcon}
${escapeHtml(proc.name)}
${proc.cpu.toFixed(1)}%
${memoryStr}
${escapeHtml(proc.username || "-")}
`;
row.onclick = (e) => {
if (e.target.classList.contains("tree-icon")) {
toggleCollapse(proc.pid);
return;
}
selectProcess(proc.pid);
};
row.ondblclick = () => {
selectProcess(proc.pid);
killProcess();
};
return row;
}
function toggleCollapse(pid) {
if (collapsedPids.has(pid)) {
collapsedPids.delete(pid);
} else {
collapsedPids.add(pid);
}
renderProcesses();
}
function selectProcess(pid) {
selectedPid = pid;
updateKillButton();
renderProcesses();
}
function killProcess() {
if (!selectedPid) return;
const proc = processes.find((p) => p.pid === selectedPid);
if (!proc) return;
if (!confirm(`Kill process "${proc.name}" (PID: ${proc.pid})?`)) return;
const pid = Number(selectedPid);
if (!Number.isFinite(pid) || pid <= 0) {
alert("Invalid PID selected.");
return;
}
console.log("Killing process:", pid);
send({ type: "process_kill", pid });
updateStatus("connected", "Killing process...");
}
function handleCommandResult(msg) {
if (!msg.ok) {
alert(`Operation failed: ${msg.message || "Unknown error"}`);
updateStatus("connected", "Connected");
} else {
setTimeout(() => requestProcessList(), 500);
}
}
function setSortField(field) {
if (sortField === field) {
sortDirection = sortDirection === "asc" ? "desc" : "asc";
} else {
sortField = field;
sortDirection = field === "name" ? "asc" : "desc";
}
buildProcessTree();
renderProcesses();
updateSortIndicators();
}
function updateSortIndicators() {
document.querySelectorAll('[id^="sort-"]').forEach((el) => {
const field = el.id.replace("sort-", "");
const icon = el.querySelector("i");
if (field === sortField) {
icon.className =
sortDirection === "asc"
? "fa-solid fa-sort-up"
: "fa-solid fa-sort-down";
} else {
icon.className = "fa-solid fa-sort";
}
});
}
function formatBytes(bytes) {
if (bytes === 0 || bytes === 0n) return "0 B";
const sizes = ["B", "KB", "MB", "GB"];
if (typeof bytes === "bigint") {
const k = 1024n;
let i = 0;
let value = bytes;
while (value >= k && i < sizes.length - 1) {
value /= k;
i += 1;
}
return `${value.toString()} ${sizes[i]}`;
}
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
refreshBtn.onclick = () => requestProcessList();
killBtn.onclick = () => killProcess();
searchInput.oninput = (e) => {
searchTerm = e.target.value;
renderProcesses();
};
document.getElementById("sort-pid").onclick = () => setSortField("pid");
document.getElementById("sort-name").onclick = () => setSortField("name");
document.getElementById("sort-cpu").onclick = () => setSortField("cpu");
document.getElementById("sort-memory").onclick = () => setSortField("memory");
setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
requestProcessList();
}
}, 3000);
updateStatus("connecting", "Connecting...");
connect();
updateSortIndicators();