fix some UI bugs, warnings and some preload issues.

This commit is contained in:
KingKDot
2026-07-15 10:34:43 -05:00
parent c577d8b2b6
commit 22b0eed5e1
12 changed files with 197 additions and 40 deletions
+2 -2
View File
@@ -138,7 +138,7 @@ async function prefetchClientPage(page) {
const key = params.toString();
if (pageCache.has(key)) return;
try {
const res = await fetch(`/api/clients?${key}`);
const res = await fetch(`/api/clients?${key}`, { credentials: "include" });
if (!res.ok) return;
rememberPage(key, await res.json());
} catch {}
@@ -169,7 +169,7 @@ export async function loadWithOptions(options = {}) {
render(cached, { reorder, fromCache: true });
}
const res = await fetch(`/api/clients?${cacheKey}`);
const res = await fetch(`/api/clients?${cacheKey}`, { credentials: "include" });
if (!res.ok) throw new Error(`status ${res.status}`);
const data = await res.json();
lastClientData = data;
+2 -1
View File
@@ -218,7 +218,8 @@ editModal?.addEventListener("click", (e) => {
if (e.target === editModal) editModal.classList.add("hidden");
});
document.getElementById("edit-save")?.addEventListener("click", async () => {
document.getElementById("edit-form")?.addEventListener("submit", async (event) => {
event.preventDefault();
const fileId = editFileId.value;
const body = {};
+137
View File
@@ -5134,6 +5134,122 @@ body.sb-ready.nav-hidden #nav-reveal-btn {
opacity: 1;
}
.plugins-layout,
.plugin-panel,
#plugin-list,
.plugin-card,
.plugin-card-meta,
.plugin-card-title,
.plugin-card-subtitle,
.plugin-card-actions,
.trusted-key-row {
min-width: 0;
max-width: 100%;
}
.plugin-panel {
overflow: hidden;
}
.plugin-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px 16px;
}
.plugin-card-title,
.plugin-card-actions {
flex-wrap: wrap;
}
.plugin-card-subtitle,
.plugin-card-meta .font-mono,
.plugin-modal-panel .font-mono {
overflow-wrap: anywhere;
word-break: break-word;
}
.plugin-card-actions {
justify-content: flex-end;
}
.plugin-card-error {
grid-column: 1 / -1;
overflow-wrap: anywhere;
}
#trusted-key-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
}
.trusted-key-row > .font-mono {
min-width: 0;
}
.plugin-modal {
overflow-y: auto;
padding: 16px 0;
}
.plugin-modal-panel {
max-width: calc(100vw - 24px);
max-height: calc(100dvh - 32px);
overflow-x: hidden;
overflow-y: auto;
}
@media (max-width: 720px) {
.plugins-page {
padding-inline: 12px !important;
}
.plugin-panel {
padding: 16px !important;
}
.plugin-section-heading {
align-items: flex-start;
flex-wrap: wrap;
}
.plugin-card {
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
}
.plugin-card-actions {
justify-content: flex-start;
}
.plugin-card-actions > button {
flex: 1 1 9rem;
justify-content: center;
}
#trusted-key-form {
grid-template-columns: minmax(0, 1fr);
}
#add-trusted-key-btn {
justify-content: center;
width: 100%;
}
.plugin-modal-panel {
padding: 18px !important;
}
.plugin-modal-actions {
flex-wrap: wrap;
}
.plugin-modal-actions > button {
flex: 1 1 9rem;
}
}
/* Keep dashboard controls below the app navigation while scrolling. */
#top-nav,
html.nav-pre-sidebar #top-nav,
@@ -5157,6 +5273,27 @@ body.sb-ready #top-nav {
gap: 8px;
}
/* Hide the overview when its actual content column is too narrow for the
five-card GridStack layout (for example beside the desktop sidebar). */
#clients-shell {
container-type: inline-size;
}
@container (max-width: 960px) {
#clients-shell > .dashboard-stats-shell {
display: none !important;
}
}
/* Viewport fallback for browsers without container-query support. */
@supports not (container-type: inline-size) {
@media (max-width: 1100px) {
.dashboard-stats-shell {
display: none !important;
}
}
}
.dashboard-stats-bar {
display: flex;
align-items: center;
+2 -2
View File
@@ -529,7 +529,7 @@ function applyFeaturePermissionRules() {
async function loadCurrentUser() {
loadServerVersion();
try {
const res = await fetch("/api/auth/me");
const res = await fetch("/api/auth/me", { credentials: "include" });
if (res.ok) {
currentUser = await res.json();
if (currentUser && currentUser.username && currentUser.role) {
@@ -1705,7 +1705,7 @@ window.setClientGroup = async (clientId, groupId) => {
async function loadGroups() {
try {
const res = await fetch("/api/groups");
const res = await fetch("/api/groups", { credentials: "include" });
if (!res.ok) return [];
const data = await res.json();
return data.groups || [];
@@ -52,5 +52,8 @@ export function createMonacoEditorAdapter(editor, monaco) {
focus() {
editor.focus();
},
dispose() {
editor.dispose();
},
};
}
+13 -4
View File
@@ -85,6 +85,7 @@ let defaultWebhookTemplate = "";
let defaultTelegramTemplate = "";
let webhookTemplateEditor = null;
let notificationTable = null;
let notificationTableReady = false;
let pageActive = true;
const subscriptionCleanups = [];
@@ -102,6 +103,7 @@ async function initWebhookTemplateEditor() {
if (!webhookTemplateInput) return;
try {
const monaco = await loadMonaco();
if (!pageActive || !webhookTemplateInput.isConnected) return;
const host = document.createElement("div");
host.className = "w-full rounded-lg border border-slate-800 overflow-hidden";
host.style.height = "180px";
@@ -344,7 +346,7 @@ function applyTableView() {
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
filtered.sort(compareEntries);
const rows = filtered.map(entryToRow);
if (notificationTable && tableEl?.isConnected) {
if (notificationTable && notificationTableReady && tableEl?.isConnected) {
void notificationTable.replaceData(rows).catch((err) => {
if (pageActive) console.error("Failed to update notification table:", err);
});
@@ -481,7 +483,7 @@ function previewFormatter(cell) {
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) {
if (res.status === 404 && attempts < maxAttempts) {
if ((res.status === 202 || res.status === 404) && attempts < maxAttempts) {
setTimeout(fetchPreview, 1000 * attempts);
return;
}
@@ -568,7 +570,7 @@ function createNotificationRowElement(item, isLive) {
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) {
if (res.status === 404 && attempts < maxAttempts) {
if ((res.status === 202 || res.status === 404) && attempts < maxAttempts) {
setTimeout(fetchPreview, 1000 * attempts);
return;
}
@@ -909,7 +911,7 @@ function connect() {
subscriptionCleanups.push(subscribeReady((history) => {
if (!pageActive) return;
clearTable();
tableState.entries.clear();
for (const item of history) {
upsertEntry(item, "notification", false);
}
@@ -1085,6 +1087,7 @@ function wireTableControls() {
function initNotificationTable() {
if (!tableEl) return;
notificationTableReady = false;
notificationTable = new Tabulator(tableEl, {
data: [],
height: "24rem",
@@ -1156,6 +1159,11 @@ function initNotificationTable() {
},
],
});
notificationTable.on("tableBuilt", () => {
if (!pageActive || !tableEl.isConnected) return;
notificationTableReady = true;
applyTableView();
});
}
// ── Bootstrap ─────────────────────────────────────────────────────────────────
@@ -1177,6 +1185,7 @@ connect();
window.addEventListener("pagehide", () => {
pageActive = false;
notificationTableReady = false;
for (const unsubscribe of subscriptionCleanups.splice(0)) unsubscribe();
webhookTemplateEditor?.dispose?.();
webhookTemplateEditor = null;
+14 -13
View File
@@ -126,11 +126,12 @@ function renderPlugins(plugins) {
const card = document.createElement("div");
card.className =
isServerOnly
? "rounded-xl border border-fuchsia-800/60 bg-fuchsia-950/20 px-4 py-3 flex items-center justify-between"
: "rounded-xl border border-slate-800 bg-slate-950/50 px-4 py-3 flex items-center justify-between";
? "plugin-card rounded-xl border border-fuchsia-800/60 bg-fuchsia-950/20 px-4 py-3"
: "plugin-card rounded-xl border border-slate-800 bg-slate-950/50 px-4 py-3";
const meta = document.createElement("div");
meta.className = "plugin-card-meta";
const titleRow = document.createElement("div");
titleRow.className = "flex items-center gap-2";
titleRow.className = "plugin-card-title flex items-center gap-2";
const title = document.createElement("span");
title.className = "font-semibold";
title.textContent = plugin.name || plugin.id;
@@ -166,7 +167,7 @@ function renderPlugins(plugins) {
titleRow.appendChild(runtimeBadge);
const subtitle = document.createElement("div");
subtitle.className = "text-sm text-slate-400";
subtitle.className = "plugin-card-subtitle text-sm text-slate-400";
subtitle.textContent = `${plugin.id}${plugin.version ? ` • v${plugin.version}` : ""}`;
if (plugin.signature?.fingerprint) {
@@ -218,7 +219,7 @@ function renderPlugins(plugins) {
meta.appendChild(serverBox);
}
const actions = document.createElement("div");
actions.className = "flex items-center gap-2";
actions.className = "plugin-card-actions flex items-center gap-2";
const toggle = document.createElement("button");
toggle.className =
@@ -353,7 +354,7 @@ function renderPlugins(plugins) {
if (plugin.lastError) {
const errorRow = document.createElement("div");
errorRow.className = "mt-2 text-xs text-red-300";
errorRow.className = "plugin-card-error mt-2 text-xs text-red-300";
errorRow.textContent = `Last error: ${plugin.lastError}`;
card.appendChild(errorRow);
}
@@ -382,9 +383,9 @@ function showNeedsApprovalModal(plugin, needs, needsHash, afterApprove) {
const needLines = describeNeeds(needs);
const overlay = document.createElement("div");
overlay.id = "plugin-needs-approval-modal";
overlay.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
overlay.className = "plugin-modal fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
overlay.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-lg w-full mx-4 shadow-2xl">
<div class="plugin-modal-panel bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-lg w-full mx-4 shadow-2xl">
<div class="flex items-center gap-3 mb-4">
<i class="fa-solid fa-key text-2xl text-amber-300"></i>
<h3 class="text-lg font-semibold text-slate-100">Approve Plugin Needs</h3>
@@ -394,7 +395,7 @@ function showNeedsApprovalModal(plugin, needs, needsHash, afterApprove) {
${needLines.length ? needLines.map((line) => `<div class="text-xs font-mono text-slate-300">${escapeHtml(line)}</div>`).join("") : '<div class="text-sm text-slate-400">No filesystem needs declared.</div>'}
</div>
${needsHash ? `<p class="text-xs text-slate-500 font-mono mb-4">Needs hash: ${escapeHtml(needsHash.slice(0, 16))}...</p>` : ""}
<div class="flex justify-end gap-2">
<div class="plugin-modal-actions flex justify-end gap-2">
<button id="plugin-needs-cancel" class="px-4 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-300 hover:bg-slate-700">Cancel</button>
<button id="plugin-needs-ok" class="px-4 py-2 rounded-lg bg-amber-900/40 border border-amber-700/60 text-amber-100 hover:bg-amber-800/60">Approve</button>
</div>
@@ -437,9 +438,9 @@ function showPluginEnableConfirmModal(plugin, sigInfo) {
const overlay = document.createElement("div");
overlay.id = "plugin-enable-confirm-modal";
overlay.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
overlay.className = "plugin-modal fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
overlay.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl">
<div class="plugin-modal-panel bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl">
<div class="flex items-center gap-3 mb-4">
<i class="fa-solid fa-triangle-exclamation text-2xl ${statusColor}"></i>
<h3 class="text-lg font-semibold text-slate-100">Enable Unverified Plugin</h3>
@@ -449,7 +450,7 @@ function showPluginEnableConfirmModal(plugin, sigInfo) {
${sig.fingerprint ? `<p class="text-xs text-slate-500 font-mono mb-3">Signer: ${sig.fingerprint}</p>` : '<p class="text-xs text-slate-500 mb-3">No signature present.</p>'}
<p class="text-sm text-slate-400" style="margin-bottom: 16px;">Type <strong class="text-emerald-300">confirm</strong> below to enable this plugin:</p>
<input type="text" id="plugin-enable-confirm-input" class="w-full px-3 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-100 text-sm focus:outline-none focus:border-emerald-500" style="margin-bottom: 16px;" placeholder="Type confirm" autocomplete="off" spellcheck="false" />
<div class="flex justify-end gap-2">
<div class="plugin-modal-actions flex justify-end gap-2">
<button id="plugin-enable-confirm-cancel" class="px-4 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-300 hover:bg-slate-700">Cancel</button>
<button id="plugin-enable-confirm-ok" disabled class="px-4 py-2 rounded-lg bg-emerald-900/40 border border-emerald-700/60 text-emerald-100 opacity-50 cursor-not-allowed">Enable Anyway</button>
</div>
@@ -587,7 +588,7 @@ function renderTrustedKeys(keys) {
for (const key of keys) {
const isBuiltin = _builtinKeys.includes(key);
const row = document.createElement("div");
row.className = "flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-800 bg-slate-950/50";
row.className = "trusted-key-row flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-800 bg-slate-950/50";
const fp = document.createElement("span");
fp.className = "font-mono text-sm text-slate-300 flex-1 truncate";
fp.textContent = key;
+8 -8
View File
@@ -42,7 +42,7 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div data-controller="toggle">
<div class="flex items-center gap-1.5 mb-1">
<label class="block text-sm font-medium text-slate-300">Password (optional)</label>
<label for="upload-password" class="block text-sm font-medium text-slate-300">Password (optional)</label>
<button type="button" id="password-info-btn" data-action="toggle#toggle" aria-expanded="false" aria-controls="password-info-panel"
class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-slate-700 hover:bg-slate-600 text-slate-400 hover:text-slate-200 text-xs transition-colors"
title="How to download with a password">
@@ -50,7 +50,7 @@
</button>
</div>
<div class="relative" data-controller="reveal">
<input type="password" id="upload-password" placeholder="Leave empty for no password" data-reveal-target="input"
<input type="password" id="upload-password" name="password" autocomplete="new-password" placeholder="Leave empty for no password" data-reveal-target="input"
class="w-full pr-10 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
<button type="button" data-action="reveal#toggle" aria-label="Show value" aria-pressed="false" class="absolute inset-y-0 right-0 px-3 text-slate-400 hover:text-slate-200">
<i class="fa-solid fa-eye" data-reveal-target="icon"></i>
@@ -124,16 +124,16 @@
<!-- Edit Modal -->
<div id="edit-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6">
<form id="edit-form" class="bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6">
<h3 class="text-lg font-semibold text-slate-100 mb-4">
<i class="fa-solid fa-pen-to-square text-rose-400 mr-2"></i>Edit File Settings
</h3>
<input type="hidden" id="edit-file-id" />
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Password</label>
<label for="edit-password" class="block text-sm font-medium text-slate-300 mb-1">Password</label>
<div class="relative" data-controller="reveal">
<input type="password" id="edit-password" placeholder="Leave empty to remove password" data-reveal-target="input"
<input type="password" id="edit-password" name="password" autocomplete="new-password" placeholder="Leave empty to remove password" data-reveal-target="input"
class="w-full pr-10 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
<button type="button" data-action="reveal#toggle" aria-label="Show value" aria-pressed="false" class="absolute inset-y-0 right-0 px-3 text-slate-400 hover:text-slate-200">
<i class="fa-solid fa-eye" data-reveal-target="icon"></i>
@@ -166,16 +166,16 @@
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button id="edit-cancel"
<button id="edit-cancel" type="button"
class="px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm transition-colors">
Cancel
</button>
<button id="edit-save"
<button id="edit-save" type="submit"
class="px-4 py-2 rounded-lg bg-rose-600 hover:bg-rose-500 text-white text-sm font-medium transition-colors">
Save Changes
</button>
</div>
</div>
</form>
</div>
<script src="/assets/nav.js" type="module"></script>
-1
View File
@@ -19,7 +19,6 @@
<script defer src="/vendor/animejs/anime.min.js"></script>
<script defer src="/vendor/chart.js/chart.umd.js"></script>
<script defer src="/vendor/gridstack/gridstack-all.js"></script>
<link rel="preload" href="/api/clients?page=1&amp;pageSize=12&amp;q=&amp;sort=stable&amp;status=all&amp;os=all&amp;country=all&amp;group=all&amp;webcam=all" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/auth/me" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/groups" as="fetch" crossorigin="use-credentials" />
</head>
+8 -8
View File
@@ -17,14 +17,14 @@
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
<header id="top-nav"></header>
<main class="px-5 py-6">
<div class="max-w-5xl mx-auto flex flex-col gap-5">
<main class="plugins-page px-5 py-6">
<div class="plugins-layout max-w-5xl mx-auto flex flex-col gap-5">
<div>
<h1 class="text-2xl font-semibold">Plugins</h1>
<p class="text-slate-400">Upload zip bundles and manage client plugins or server extensions</p>
</div>
<section class="rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<section class="plugin-panel rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<div class="flex items-center justify-between gap-3 flex-wrap">
<div>
<h2 class="text-lg font-semibold">Upload</h2>
@@ -47,8 +47,8 @@
<div id="upload-status" class="mt-3 text-sm text-slate-400"></div>
</section>
<section class="rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<div class="flex items-center justify-between">
<section class="plugin-panel rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<div class="plugin-section-heading flex items-center justify-between gap-3">
<h2 class="text-lg font-semibold">Installed plugins</h2>
<button
id="refresh-btn"
@@ -60,16 +60,16 @@
<div id="plugin-list" class="mt-4 grid gap-3"></div>
</section>
<section id="trusted-keys-section" class="hidden rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<section id="trusted-keys-section" class="plugin-panel hidden rounded-2xl border border-slate-800 bg-slate-900/50 p-5">
<h2 class="text-lg font-semibold"><i class="fa-solid fa-key mr-2"></i>Trusted Signing Keys</h2>
<p class="text-slate-400 text-sm mt-1">Plugins signed by a trusted key can be loaded without confirmation. Add key fingerprints (SHA-256 hex) here.</p>
<div id="trusted-keys-list" class="mt-4 grid gap-2"></div>
<div class="mt-4 flex gap-2">
<div id="trusted-key-form" class="mt-4 flex gap-2">
<input
id="new-trusted-key-input"
type="text"
placeholder="Paste 64-char hex fingerprint…"
class="flex-1 px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 text-slate-100 font-mono text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-600"
class="min-w-0 flex-1 px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 text-slate-100 font-mono text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-600"
maxlength="64"
/>
<button
+1
View File
@@ -170,6 +170,7 @@
type="password"
id="password"
name="password"
autocomplete="new-password"
required
minlength="6"
class="w-full px-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
@@ -45,7 +45,13 @@ export async function handleNotificationsConfigRoutes(
const notificationId = decodeURIComponent(screenshotMatch[1]);
const screenshot = deps.getNotificationScreenshot(notificationId);
if (!screenshot) {
return new Response("Not found", { status: 404 });
return new Response(null, {
status: 202,
headers: {
"Cache-Control": "no-store, private",
"Retry-After": "1",
},
});
}
if (