fix some UI bugs, warnings and some preload issues.
This commit is contained in:
@@ -138,7 +138,7 @@ async function prefetchClientPage(page) {
|
|||||||
const key = params.toString();
|
const key = params.toString();
|
||||||
if (pageCache.has(key)) return;
|
if (pageCache.has(key)) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/clients?${key}`);
|
const res = await fetch(`/api/clients?${key}`, { credentials: "include" });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
rememberPage(key, await res.json());
|
rememberPage(key, await res.json());
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -169,7 +169,7 @@ export async function loadWithOptions(options = {}) {
|
|||||||
render(cached, { reorder, fromCache: true });
|
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}`);
|
if (!res.ok) throw new Error(`status ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
lastClientData = data;
|
lastClientData = data;
|
||||||
|
|||||||
@@ -218,7 +218,8 @@ editModal?.addEventListener("click", (e) => {
|
|||||||
if (e.target === editModal) editModal.classList.add("hidden");
|
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 fileId = editFileId.value;
|
||||||
const body = {};
|
const body = {};
|
||||||
|
|
||||||
|
|||||||
@@ -5134,6 +5134,122 @@ body.sb-ready.nav-hidden #nav-reveal-btn {
|
|||||||
opacity: 1;
|
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. */
|
/* Keep dashboard controls below the app navigation while scrolling. */
|
||||||
#top-nav,
|
#top-nav,
|
||||||
html.nav-pre-sidebar #top-nav,
|
html.nav-pre-sidebar #top-nav,
|
||||||
@@ -5157,6 +5273,27 @@ body.sb-ready #top-nav {
|
|||||||
gap: 8px;
|
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 {
|
.dashboard-stats-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ function applyFeaturePermissionRules() {
|
|||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
loadServerVersion();
|
loadServerVersion();
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/auth/me");
|
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
currentUser = await res.json();
|
currentUser = await res.json();
|
||||||
if (currentUser && currentUser.username && currentUser.role) {
|
if (currentUser && currentUser.username && currentUser.role) {
|
||||||
@@ -1705,7 +1705,7 @@ window.setClientGroup = async (clientId, groupId) => {
|
|||||||
|
|
||||||
async function loadGroups() {
|
async function loadGroups() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/groups");
|
const res = await fetch("/api/groups", { credentials: "include" });
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.groups || [];
|
return data.groups || [];
|
||||||
|
|||||||
@@ -52,5 +52,8 @@ export function createMonacoEditorAdapter(editor, monaco) {
|
|||||||
focus() {
|
focus() {
|
||||||
editor.focus();
|
editor.focus();
|
||||||
},
|
},
|
||||||
|
dispose() {
|
||||||
|
editor.dispose();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ let defaultWebhookTemplate = "";
|
|||||||
let defaultTelegramTemplate = "";
|
let defaultTelegramTemplate = "";
|
||||||
let webhookTemplateEditor = null;
|
let webhookTemplateEditor = null;
|
||||||
let notificationTable = null;
|
let notificationTable = null;
|
||||||
|
let notificationTableReady = false;
|
||||||
let pageActive = true;
|
let pageActive = true;
|
||||||
const subscriptionCleanups = [];
|
const subscriptionCleanups = [];
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ async function initWebhookTemplateEditor() {
|
|||||||
if (!webhookTemplateInput) return;
|
if (!webhookTemplateInput) return;
|
||||||
try {
|
try {
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
|
if (!pageActive || !webhookTemplateInput.isConnected) return;
|
||||||
const host = document.createElement("div");
|
const host = document.createElement("div");
|
||||||
host.className = "w-full rounded-lg border border-slate-800 overflow-hidden";
|
host.className = "w-full rounded-lg border border-slate-800 overflow-hidden";
|
||||||
host.style.height = "180px";
|
host.style.height = "180px";
|
||||||
@@ -344,7 +346,7 @@ function applyTableView() {
|
|||||||
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
|
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
|
||||||
filtered.sort(compareEntries);
|
filtered.sort(compareEntries);
|
||||||
const rows = filtered.map(entryToRow);
|
const rows = filtered.map(entryToRow);
|
||||||
if (notificationTable && tableEl?.isConnected) {
|
if (notificationTable && notificationTableReady && tableEl?.isConnected) {
|
||||||
void notificationTable.replaceData(rows).catch((err) => {
|
void notificationTable.replaceData(rows).catch((err) => {
|
||||||
if (pageActive) console.error("Failed to update notification table:", 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 url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
|
||||||
const res = await fetch(url, { cache: "no-store" });
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404 && attempts < maxAttempts) {
|
if ((res.status === 202 || res.status === 404) && attempts < maxAttempts) {
|
||||||
setTimeout(fetchPreview, 1000 * attempts);
|
setTimeout(fetchPreview, 1000 * attempts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -568,7 +570,7 @@ function createNotificationRowElement(item, isLive) {
|
|||||||
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
|
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
|
||||||
const res = await fetch(url, { cache: "no-store" });
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 404 && attempts < maxAttempts) {
|
if ((res.status === 202 || res.status === 404) && attempts < maxAttempts) {
|
||||||
setTimeout(fetchPreview, 1000 * attempts);
|
setTimeout(fetchPreview, 1000 * attempts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -909,7 +911,7 @@ function connect() {
|
|||||||
|
|
||||||
subscriptionCleanups.push(subscribeReady((history) => {
|
subscriptionCleanups.push(subscribeReady((history) => {
|
||||||
if (!pageActive) return;
|
if (!pageActive) return;
|
||||||
clearTable();
|
tableState.entries.clear();
|
||||||
for (const item of history) {
|
for (const item of history) {
|
||||||
upsertEntry(item, "notification", false);
|
upsertEntry(item, "notification", false);
|
||||||
}
|
}
|
||||||
@@ -1085,6 +1087,7 @@ function wireTableControls() {
|
|||||||
|
|
||||||
function initNotificationTable() {
|
function initNotificationTable() {
|
||||||
if (!tableEl) return;
|
if (!tableEl) return;
|
||||||
|
notificationTableReady = false;
|
||||||
notificationTable = new Tabulator(tableEl, {
|
notificationTable = new Tabulator(tableEl, {
|
||||||
data: [],
|
data: [],
|
||||||
height: "24rem",
|
height: "24rem",
|
||||||
@@ -1156,6 +1159,11 @@ function initNotificationTable() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
notificationTable.on("tableBuilt", () => {
|
||||||
|
if (!pageActive || !tableEl.isConnected) return;
|
||||||
|
notificationTableReady = true;
|
||||||
|
applyTableView();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||||
@@ -1177,6 +1185,7 @@ connect();
|
|||||||
|
|
||||||
window.addEventListener("pagehide", () => {
|
window.addEventListener("pagehide", () => {
|
||||||
pageActive = false;
|
pageActive = false;
|
||||||
|
notificationTableReady = false;
|
||||||
for (const unsubscribe of subscriptionCleanups.splice(0)) unsubscribe();
|
for (const unsubscribe of subscriptionCleanups.splice(0)) unsubscribe();
|
||||||
webhookTemplateEditor?.dispose?.();
|
webhookTemplateEditor?.dispose?.();
|
||||||
webhookTemplateEditor = null;
|
webhookTemplateEditor = null;
|
||||||
|
|||||||
@@ -126,11 +126,12 @@ function renderPlugins(plugins) {
|
|||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className =
|
card.className =
|
||||||
isServerOnly
|
isServerOnly
|
||||||
? "rounded-xl border border-fuchsia-800/60 bg-fuchsia-950/20 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"
|
||||||
: "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-slate-800 bg-slate-950/50 px-4 py-3";
|
||||||
const meta = document.createElement("div");
|
const meta = document.createElement("div");
|
||||||
|
meta.className = "plugin-card-meta";
|
||||||
const titleRow = document.createElement("div");
|
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");
|
const title = document.createElement("span");
|
||||||
title.className = "font-semibold";
|
title.className = "font-semibold";
|
||||||
title.textContent = plugin.name || plugin.id;
|
title.textContent = plugin.name || plugin.id;
|
||||||
@@ -166,7 +167,7 @@ function renderPlugins(plugins) {
|
|||||||
titleRow.appendChild(runtimeBadge);
|
titleRow.appendChild(runtimeBadge);
|
||||||
|
|
||||||
const subtitle = document.createElement("div");
|
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}` : ""}`;
|
subtitle.textContent = `${plugin.id}${plugin.version ? ` • v${plugin.version}` : ""}`;
|
||||||
|
|
||||||
if (plugin.signature?.fingerprint) {
|
if (plugin.signature?.fingerprint) {
|
||||||
@@ -218,7 +219,7 @@ function renderPlugins(plugins) {
|
|||||||
meta.appendChild(serverBox);
|
meta.appendChild(serverBox);
|
||||||
}
|
}
|
||||||
const actions = document.createElement("div");
|
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");
|
const toggle = document.createElement("button");
|
||||||
toggle.className =
|
toggle.className =
|
||||||
@@ -353,7 +354,7 @@ function renderPlugins(plugins) {
|
|||||||
|
|
||||||
if (plugin.lastError) {
|
if (plugin.lastError) {
|
||||||
const errorRow = document.createElement("div");
|
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}`;
|
errorRow.textContent = `Last error: ${plugin.lastError}`;
|
||||||
card.appendChild(errorRow);
|
card.appendChild(errorRow);
|
||||||
}
|
}
|
||||||
@@ -382,9 +383,9 @@ function showNeedsApprovalModal(plugin, needs, needsHash, afterApprove) {
|
|||||||
const needLines = describeNeeds(needs);
|
const needLines = describeNeeds(needs);
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement("div");
|
||||||
overlay.id = "plugin-needs-approval-modal";
|
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 = `
|
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">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<i class="fa-solid fa-key text-2xl text-amber-300"></i>
|
<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>
|
<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>'}
|
${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>
|
</div>
|
||||||
${needsHash ? `<p class="text-xs text-slate-500 font-mono mb-4">Needs hash: ${escapeHtml(needsHash.slice(0, 16))}...</p>` : ""}
|
${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-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>
|
<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>
|
</div>
|
||||||
@@ -437,9 +438,9 @@ function showPluginEnableConfirmModal(plugin, sigInfo) {
|
|||||||
|
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement("div");
|
||||||
overlay.id = "plugin-enable-confirm-modal";
|
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 = `
|
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">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<i class="fa-solid fa-triangle-exclamation text-2xl ${statusColor}"></i>
|
<i class="fa-solid fa-triangle-exclamation text-2xl ${statusColor}"></i>
|
||||||
<h3 class="text-lg font-semibold text-slate-100">Enable Unverified Plugin</h3>
|
<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>'}
|
${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>
|
<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" />
|
<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-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>
|
<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>
|
</div>
|
||||||
@@ -587,7 +588,7 @@ function renderTrustedKeys(keys) {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const isBuiltin = _builtinKeys.includes(key);
|
const isBuiltin = _builtinKeys.includes(key);
|
||||||
const row = document.createElement("div");
|
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");
|
const fp = document.createElement("span");
|
||||||
fp.className = "font-mono text-sm text-slate-300 flex-1 truncate";
|
fp.className = "font-mono text-sm text-slate-300 flex-1 truncate";
|
||||||
fp.textContent = key;
|
fp.textContent = key;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div data-controller="toggle">
|
<div data-controller="toggle">
|
||||||
<div class="flex items-center gap-1.5 mb-1">
|
<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"
|
<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"
|
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">
|
title="How to download with a password">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative" data-controller="reveal">
|
<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" />
|
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">
|
<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>
|
<i class="fa-solid fa-eye" data-reveal-target="icon"></i>
|
||||||
@@ -124,16 +124,16 @@
|
|||||||
|
|
||||||
<!-- Edit Modal -->
|
<!-- 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 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">
|
<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
|
<i class="fa-solid fa-pen-to-square text-rose-400 mr-2"></i>Edit File Settings
|
||||||
</h3>
|
</h3>
|
||||||
<input type="hidden" id="edit-file-id" />
|
<input type="hidden" id="edit-file-id" />
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<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">
|
<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" />
|
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">
|
<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>
|
<i class="fa-solid fa-eye" data-reveal-target="icon"></i>
|
||||||
@@ -166,16 +166,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-3 mt-6">
|
<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">
|
class="px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm transition-colors">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</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">
|
class="px-4 py-2 rounded-lg bg-rose-600 hover:bg-rose-500 text-white text-sm font-medium transition-colors">
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/nav.js" type="module"></script>
|
<script src="/assets/nav.js" type="module"></script>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
<script defer src="/vendor/animejs/anime.min.js"></script>
|
<script defer src="/vendor/animejs/anime.min.js"></script>
|
||||||
<script defer src="/vendor/chart.js/chart.umd.js"></script>
|
<script defer src="/vendor/chart.js/chart.umd.js"></script>
|
||||||
<script defer src="/vendor/gridstack/gridstack-all.js"></script>
|
<script defer src="/vendor/gridstack/gridstack-all.js"></script>
|
||||||
<link rel="preload" href="/api/clients?page=1&pageSize=12&q=&sort=stable&status=all&os=all&country=all&group=all&webcam=all" as="fetch" crossorigin="use-credentials" />
|
|
||||||
<link rel="preload" href="/api/auth/me" 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" />
|
<link rel="preload" href="/api/groups" as="fetch" crossorigin="use-credentials" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-slate-950 text-slate-100">
|
<body class="min-h-screen bg-slate-950 text-slate-100">
|
||||||
<header id="top-nav"></header>
|
<header id="top-nav"></header>
|
||||||
<main class="px-5 py-6">
|
<main class="plugins-page px-5 py-6">
|
||||||
<div class="max-w-5xl mx-auto flex flex-col gap-5">
|
<div class="plugins-layout max-w-5xl mx-auto flex flex-col gap-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold">Plugins</h1>
|
<h1 class="text-2xl font-semibold">Plugins</h1>
|
||||||
<p class="text-slate-400">Upload zip bundles and manage client plugins or server extensions</p>
|
<p class="text-slate-400">Upload zip bundles and manage client plugins or server extensions</p>
|
||||||
</div>
|
</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 class="flex items-center justify-between gap-3 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold">Upload</h2>
|
<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>
|
<div id="upload-status" class="mt-3 text-sm text-slate-400"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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">
|
<div class="plugin-section-heading flex items-center justify-between gap-3">
|
||||||
<h2 class="text-lg font-semibold">Installed plugins</h2>
|
<h2 class="text-lg font-semibold">Installed plugins</h2>
|
||||||
<button
|
<button
|
||||||
id="refresh-btn"
|
id="refresh-btn"
|
||||||
@@ -60,16 +60,16 @@
|
|||||||
<div id="plugin-list" class="mt-4 grid gap-3"></div>
|
<div id="plugin-list" class="mt-4 grid gap-3"></div>
|
||||||
</section>
|
</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>
|
<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>
|
<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 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
|
<input
|
||||||
id="new-trusted-key-input"
|
id="new-trusted-key-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Paste 64-char hex fingerprint…"
|
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"
|
maxlength="64"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
autocomplete="new-password"
|
||||||
required
|
required
|
||||||
minlength="6"
|
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"
|
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 notificationId = decodeURIComponent(screenshotMatch[1]);
|
||||||
const screenshot = deps.getNotificationScreenshot(notificationId);
|
const screenshot = deps.getNotificationScreenshot(notificationId);
|
||||||
if (!screenshot) {
|
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 (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user