upgrade UI for process manager + logs.
This commit is contained in:
@@ -8,26 +8,28 @@ const refreshBtn = document.getElementById("refresh-btn");
|
||||
const prevPageBtn = document.getElementById("prev-page");
|
||||
const nextPageBtn = document.getElementById("next-page");
|
||||
const pageInfo = document.getElementById("page-info");
|
||||
const logsCount = document.getElementById("logs-count");
|
||||
const clearFiltersBtn = document.getElementById("clear-filters-btn");
|
||||
|
||||
const actionLabels = {
|
||||
client_first_connect: {
|
||||
label: "First Connect",
|
||||
className: "bg-emerald-900/40 text-emerald-200 border border-emerald-800",
|
||||
className: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
|
||||
icon: "fa-plug-circle-bolt",
|
||||
},
|
||||
client_reconnect: {
|
||||
label: "Reconnect",
|
||||
className: "bg-blue-900/40 text-blue-200 border border-blue-800",
|
||||
className: "bg-blue-500/10 text-blue-300 border-blue-500/30",
|
||||
icon: "fa-rotate",
|
||||
},
|
||||
client_disconnect: {
|
||||
label: "Disconnect",
|
||||
className: "bg-orange-900/40 text-orange-200 border border-orange-800",
|
||||
className: "bg-orange-500/10 text-orange-300 border-orange-500/30",
|
||||
icon: "fa-plug-circle-xmark",
|
||||
},
|
||||
uninstall: {
|
||||
label: "Uninstall",
|
||||
className: "bg-rose-900/40 text-rose-200 border border-rose-800",
|
||||
className: "bg-rose-500/10 text-rose-300 border-rose-500/30",
|
||||
icon: "fa-trash",
|
||||
},
|
||||
};
|
||||
@@ -98,15 +100,21 @@ function renderLogs(logs) {
|
||||
logsContainer.innerHTML = "";
|
||||
if (!logs || logs.length === 0) {
|
||||
logsEmpty.classList.remove("hidden");
|
||||
if (logsCount) logsCount.textContent = "0 matching events";
|
||||
return;
|
||||
}
|
||||
logsEmpty.classList.add("hidden");
|
||||
if (logsCount) {
|
||||
const first = (page - 1) * pageSize + 1;
|
||||
const last = Math.min(first + logs.length - 1, total);
|
||||
logsCount.textContent = `${total.toLocaleString()} events · showing ${first}–${last}`;
|
||||
}
|
||||
|
||||
logsContainer.innerHTML = logs
|
||||
.map((log) => {
|
||||
const meta = actionLabels[log.action] || {
|
||||
label: log.action,
|
||||
className: "bg-slate-800/60 text-slate-200 border border-slate-700",
|
||||
className: "bg-slate-800 text-slate-300 border-slate-700",
|
||||
icon: "fa-circle-info",
|
||||
};
|
||||
const clientId = log.targetClientId || "-";
|
||||
@@ -114,25 +122,26 @@ function renderLogs(logs) {
|
||||
const detailText = parseDetails(log.details);
|
||||
|
||||
return `
|
||||
<div class="bg-slate-950/60 border border-slate-800 rounded-lg p-4 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs ${meta.className}">
|
||||
<i class="fa-solid ${meta.icon}"></i>
|
||||
${meta.label}
|
||||
</span>
|
||||
<span class="text-sm text-slate-300" title="${escapeHtml(clientId)}">
|
||||
Client: <span class="font-mono">${escapeHtml(shortId)}</span>
|
||||
</span>
|
||||
<article class="px-4 sm:px-5 py-4 flex gap-3 sm:gap-4 hover:bg-slate-800/20 transition-colors">
|
||||
<div class="inline-flex items-center justify-center w-9 h-9 shrink-0 rounded-lg border ${meta.className}">
|
||||
<i class="fa-solid ${meta.icon}"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-3">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-sm font-semibold text-slate-200">${escapeHtml(meta.label || "Event")}</span>
|
||||
<span class="text-xs text-slate-500">Client</span>
|
||||
<span class="font-mono text-xs text-sky-300 truncate" title="${escapeHtml(clientId)}">${escapeHtml(shortId)}</span>
|
||||
</div>
|
||||
<time class="text-xs text-slate-500 shrink-0">${formatTimestamp(log.timestamp)}</time>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">${formatTimestamp(log.timestamp)}</div>
|
||||
<div class="mt-1.5 text-xs text-slate-500 flex flex-wrap gap-x-4 gap-y-1">
|
||||
<span><i class="fa-solid fa-globe mr-1 text-slate-600"></i>${escapeHtml(log.ip || "-")}</span>
|
||||
<span><i class="fa-solid fa-user mr-1 text-slate-600"></i>${escapeHtml(log.username || "-")}</span>
|
||||
</div>
|
||||
${detailText ? `<div class="mt-2 rounded-md bg-slate-950/60 border border-slate-800 px-2.5 py-2 text-xs font-mono text-slate-400 break-all">${detailText}</div>` : ""}
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 flex flex-wrap gap-3">
|
||||
<span>IP: ${escapeHtml(log.ip || "-")}</span>
|
||||
<span>User: ${escapeHtml(log.username || "-")}</span>
|
||||
</div>
|
||||
${detailText ? `<div class="text-xs text-slate-400">Details: ${detailText}</div>` : ""}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
@@ -140,6 +149,18 @@ function renderLogs(logs) {
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
if (refreshBtn) refreshBtn.disabled = true;
|
||||
if (logsCount) logsCount.textContent = "Loading events…";
|
||||
if (getSelectedActions().length === 0) {
|
||||
total = 0;
|
||||
page = 1;
|
||||
renderLogs([]);
|
||||
pageInfo.textContent = "Page 1 of 1";
|
||||
prevPageBtn.disabled = true;
|
||||
nextPageBtn.disabled = true;
|
||||
lastUpdate.textContent = new Date().toLocaleTimeString();
|
||||
return;
|
||||
}
|
||||
const params = buildQuery();
|
||||
const res = await fetch(`/api/audit-logs?${params.toString()}`);
|
||||
if (!res.ok) {
|
||||
@@ -160,9 +181,12 @@ async function loadLogs() {
|
||||
console.error("Failed to load logs", err);
|
||||
if (logsContainer) {
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-rose-300 text-sm">Failed to load logs. Please try again.</div>
|
||||
<div class="px-5 py-12 text-center text-rose-300 text-sm"><i class="fa-solid fa-circle-exclamation mr-2"></i>Failed to load logs. Please try again.</div>
|
||||
`;
|
||||
}
|
||||
if (logsCount) logsCount.textContent = "Unable to load events";
|
||||
} finally {
|
||||
if (refreshBtn) refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +205,15 @@ clientFilter?.addEventListener("input", debounceLoad);
|
||||
startDateInput?.addEventListener("change", resetAndLoad);
|
||||
endDateInput?.addEventListener("change", resetAndLoad);
|
||||
refreshBtn?.addEventListener("click", resetAndLoad);
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
if (clientFilter) clientFilter.value = "";
|
||||
if (startDateInput) startDateInput.value = "";
|
||||
if (endDateInput) endDateInput.value = "";
|
||||
document.querySelectorAll(".action-checkbox").forEach((checkbox) => {
|
||||
checkbox.checked = true;
|
||||
});
|
||||
resetAndLoad();
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll(".action-checkbox")).forEach((cb) => {
|
||||
cb.addEventListener("change", resetAndLoad);
|
||||
|
||||
@@ -28,7 +28,7 @@ const searchInput = document.getElementById("search-input");
|
||||
const clientIdHeader = document.getElementById("client-id-header");
|
||||
|
||||
if (clientIdHeader) {
|
||||
clientIdHeader.textContent = `${clientId} - Process Manager`;
|
||||
clientIdHeader.innerHTML = `<i class="fa-solid fa-microchip mr-1.5 text-sky-400"></i>${escapeHtml(clientId)}`;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
@@ -76,10 +76,13 @@ function updateStatus(state, text) {
|
||||
};
|
||||
|
||||
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";
|
||||
const stateClasses = {
|
||||
connected: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
|
||||
error: "bg-red-500/10 text-red-300 border-red-500/30",
|
||||
disconnected: "bg-slate-800 text-slate-300 border-slate-700",
|
||||
connecting: "bg-sky-500/10 text-sky-300 border-sky-500/30",
|
||||
};
|
||||
statusEl.className = `inline-flex self-start sm:self-auto items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium ${stateClasses[state] || stateClasses.disconnected}`;
|
||||
}
|
||||
|
||||
function enableControls(enabled) {
|
||||
@@ -280,7 +283,7 @@ function renderProcesses() {
|
||||
|
||||
function rowClassName(proc) {
|
||||
let cls =
|
||||
"process-row grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors";
|
||||
"process-row grid grid-cols-12 gap-3 px-4 py-3 border-l-2 border-transparent cursor-pointer transition-colors";
|
||||
if (selectedPid === proc.pid) cls += " selected";
|
||||
if (proc.self) cls += " self-process";
|
||||
return cls;
|
||||
@@ -356,14 +359,14 @@ function rowInnerHtml(proc, depth) {
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
|
||||
<div class="col-span-4 flex items-center gap-1 truncate">
|
||||
<div class="process-cell process-pid col-span-1 text-sm font-mono text-slate-400" data-label="PID">${proc.pid}</div>
|
||||
<div class="process-cell process-name col-span-4 flex items-center gap-1 truncate">
|
||||
${indent}${treeIcon}${procIcon}
|
||||
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
|
||||
</div>
|
||||
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${displayCpu.toFixed(1)}%</div>
|
||||
<div class="col-span-2 text-sm ${memColor}">${memoryStr}</div>
|
||||
<div class="col-span-3 text-sm text-slate-500 truncate">${escapeHtml(proc.username || "-")}</div>
|
||||
<div class="process-cell process-cpu col-span-2 text-sm ${cpuColor} font-semibold" data-label="CPU">${displayCpu.toFixed(1)}%</div>
|
||||
<div class="process-cell process-memory col-span-2 text-sm ${memColor}" data-label="Memory">${memoryStr}</div>
|
||||
<div class="process-cell process-user col-span-3 text-sm text-slate-500 truncate" data-label="User">${escapeHtml(proc.username || "-")}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user