Full-stack implementation: FastAPI backend with PAM auth, WebSocket stats/terminal, and vanilla JS frontend with tiling desktop shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
/* Atlus — Task Manager app */
|
|
(function () {
|
|
'use strict';
|
|
|
|
let container = null;
|
|
let listEl = null;
|
|
let searchInput = null;
|
|
let processes = [];
|
|
let sortCol = 'cpu';
|
|
let sortDir = -1; // -1 = desc, 1 = asc
|
|
let refreshInterval = null;
|
|
|
|
async function loadProcesses() {
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/processes');
|
|
if (!res.ok) return;
|
|
processes = await res.json();
|
|
renderProcesses();
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderProcesses() {
|
|
if (!listEl) return;
|
|
const query = searchInput ? searchInput.value.toLowerCase() : '';
|
|
|
|
let filtered = processes;
|
|
if (query) {
|
|
filtered = filtered.filter(p =>
|
|
p.name.toLowerCase().includes(query) ||
|
|
String(p.pid).includes(query) ||
|
|
(p.user && p.user.toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
// Sort
|
|
filtered.sort((a, b) => {
|
|
const av = a[sortCol] ?? 0;
|
|
const bv = b[sortCol] ?? 0;
|
|
if (typeof av === 'string') return av.localeCompare(bv) * sortDir;
|
|
return (av - bv) * sortDir;
|
|
});
|
|
|
|
// Summary
|
|
const totalCpu = processes.reduce((s, p) => s + (p.cpu || 0), 0);
|
|
const totalMem = processes.reduce((s, p) => s + (p.mem || 0), 0);
|
|
const summaryEl = container.querySelector('.tasks-summary');
|
|
if (summaryEl) {
|
|
summaryEl.innerHTML = `
|
|
<div class="tasks-summary-item">
|
|
<span class="tasks-summary-label">PROCESSES</span>
|
|
<span class="tasks-summary-value">${processes.length}</span>
|
|
</div>
|
|
<div class="tasks-summary-item">
|
|
<span class="tasks-summary-label">CPU</span>
|
|
<span class="tasks-summary-value">${totalCpu.toFixed(1)}%</span>
|
|
</div>
|
|
<div class="tasks-summary-item">
|
|
<span class="tasks-summary-label">MEM</span>
|
|
<span class="tasks-summary-value">${totalMem.toFixed(1)}%</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
listEl.innerHTML = '';
|
|
|
|
// Header
|
|
const header = document.createElement('div');
|
|
header.className = 'tasks-header';
|
|
const cols = [
|
|
{ key: 'pid', label: 'PID' },
|
|
{ key: 'name', label: 'NAME' },
|
|
{ key: 'user', label: 'USER' },
|
|
{ key: 'cpu', label: 'CPU %' },
|
|
{ key: 'mem', label: 'MEM %' },
|
|
{ key: 'status', label: 'STATUS' },
|
|
{ key: '', label: '' },
|
|
];
|
|
cols.forEach(col => {
|
|
const span = document.createElement('span');
|
|
span.textContent = col.label;
|
|
if (col.key === sortCol) span.classList.add('sorted');
|
|
if (col.key) {
|
|
span.addEventListener('click', () => {
|
|
if (sortCol === col.key) sortDir *= -1;
|
|
else { sortCol = col.key; sortDir = -1; }
|
|
renderProcesses();
|
|
});
|
|
}
|
|
header.appendChild(span);
|
|
});
|
|
listEl.appendChild(header);
|
|
|
|
filtered.forEach(proc => {
|
|
const row = document.createElement('div');
|
|
row.className = 'proc-row';
|
|
row.innerHTML = `
|
|
<span class="proc-pid">${proc.pid}</span>
|
|
<span class="proc-name" title="${proc.cmdline || proc.name}">${proc.name}</span>
|
|
<span class="proc-user">${proc.user || '--'}</span>
|
|
<span class="proc-cpu">${(proc.cpu || 0).toFixed(1)}</span>
|
|
<span class="proc-mem">${(proc.mem || 0).toFixed(1)}</span>
|
|
<span class="proc-status">${proc.status}</span>
|
|
<button class="proc-kill" title="Kill process" data-pid="${proc.pid}">×</button>
|
|
`;
|
|
|
|
const killBtn = row.querySelector('.proc-kill');
|
|
killBtn.addEventListener('click', async () => {
|
|
if (!confirm(`Kill process ${proc.name} (PID ${proc.pid})?`)) return;
|
|
await Atlus.apiFetch('/api/processes/signal', {
|
|
method: 'POST',
|
|
body: { pid: proc.pid, signal: 'SIGTERM' },
|
|
});
|
|
setTimeout(loadProcesses, 500);
|
|
});
|
|
|
|
listEl.appendChild(row);
|
|
});
|
|
}
|
|
|
|
Atlus.registerApp('tasks', {
|
|
title: 'Task Manager',
|
|
|
|
init(el) {
|
|
container = el;
|
|
container.classList.add('app-tasks');
|
|
|
|
// Summary
|
|
const summary = document.createElement('div');
|
|
summary.className = 'tasks-summary';
|
|
container.appendChild(summary);
|
|
|
|
// Toolbar
|
|
const toolbar = document.createElement('div');
|
|
toolbar.className = 'tasks-toolbar';
|
|
|
|
searchInput = document.createElement('input');
|
|
searchInput.className = 'tasks-search';
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = 'Filter processes…';
|
|
searchInput.addEventListener('input', renderProcesses);
|
|
toolbar.appendChild(searchInput);
|
|
|
|
container.appendChild(toolbar);
|
|
|
|
// List
|
|
listEl = document.createElement('div');
|
|
listEl.className = 'tasks-list';
|
|
container.appendChild(listEl);
|
|
|
|
loadProcesses();
|
|
refreshInterval = setInterval(loadProcesses, 3000);
|
|
},
|
|
|
|
destroy() {
|
|
if (refreshInterval) clearInterval(refreshInterval);
|
|
refreshInterval = null;
|
|
container = null;
|
|
listEl = null;
|
|
searchInput = null;
|
|
processes = [];
|
|
},
|
|
});
|
|
})();
|