diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index 4dbd944..ad46283 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -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; diff --git a/Overlord-Server/public/assets/file-share.js b/Overlord-Server/public/assets/file-share.js index 041a9b4..9cbad5b 100644 --- a/Overlord-Server/public/assets/file-share.js +++ b/Overlord-Server/public/assets/file-share.js @@ -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 = {}; diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 7d63f92..e63a347 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -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; diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 96505b7..c48efa9 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -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 || []; diff --git a/Overlord-Server/public/assets/monaco-loader.js b/Overlord-Server/public/assets/monaco-loader.js index 33ee97d..ce43be1 100644 --- a/Overlord-Server/public/assets/monaco-loader.js +++ b/Overlord-Server/public/assets/monaco-loader.js @@ -52,5 +52,8 @@ export function createMonacoEditorAdapter(editor, monaco) { focus() { editor.focus(); }, + dispose() { + editor.dispose(); + }, }; } diff --git a/Overlord-Server/public/assets/notifications.js b/Overlord-Server/public/assets/notifications.js index d16ad46..8988992 100644 --- a/Overlord-Server/public/assets/notifications.js +++ b/Overlord-Server/public/assets/notifications.js @@ -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; diff --git a/Overlord-Server/public/assets/plugins.js b/Overlord-Server/public/assets/plugins.js index a8926f5..fb9f08a 100644 --- a/Overlord-Server/public/assets/plugins.js +++ b/Overlord-Server/public/assets/plugins.js @@ -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 = ` -
Needs hash: ${escapeHtml(needsHash.slice(0, 16))}...
` : ""} -Signer: ${sig.fingerprint}
` : 'No signature present.
'}Type confirm below to enable this plugin:
-