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();
|
||||
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;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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&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/groups" as="fetch" crossorigin="use-credentials" />
|
||||
</head>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user