const parts = window.location.pathname.split("/"); const serverId = BASE_PATH ? parts[2] : parts[1]; if (!serverId) { document.querySelector(".loading")?.remove(); document.getElementById("content").innerHTML += "

Invalid server

"; } function detectTab() { if (/\/anti-spam\/?$/.test(window.location.pathname)) return "anti-spam"; if (/\/vote-moderation\/?$/.test(window.location.pathname)) return "vote"; if (/\/infractions\/?$/.test(window.location.pathname)) return "infractions"; if (/\/logging\/?$/.test(window.location.pathname)) return "logging"; return "general"; } let activeTab = detectTab(); let antispamLoaded = false; let antispamLoading = false; let infractionsLoaded = false; let infractionsLoading = false; let infractionsResolving = false; let resolvedUserIds = new Set(); let rules = []; let perms = 0; function switchTab(tab, pushState = true) { if (activeTab === tab) return; activeTab = tab; const tabs = { "general": "tab-general", "anti-spam": "tab-anti-spam", "vote": "tab-vote", "infractions": "tab-infractions", "logging": "tab-logging" }; for (const [t, id] of Object.entries(tabs)) { const el = document.getElementById(id); if (el) el.hidden = t !== tab; } document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === tab)); const tabPaths = { "general": `${BASE_PATH}/${serverId}`, "anti-spam": `${BASE_PATH}/${serverId}/anti-spam`, "vote": `${BASE_PATH}/${serverId}/vote-moderation`, "infractions": `${BASE_PATH}/${serverId}/infractions`, "logging": `${BASE_PATH}/${serverId}/logging`, }; if (pushState) history.pushState({ tab }, "", tabPaths[tab]); const tabTitles = { "general": "Server Settings | AutoMod", "anti-spam": "Anti-Spam | AutoMod", "vote": "Vote Moderation | AutoMod", "infractions": "Infractions | AutoMod", "logging": "Logging | AutoMod", }; document.title = tabTitles[tab] || "Server Dashboard | AutoMod"; if (tab === "anti-spam" && !antispamLoaded) loadAntiSpam(); if (tab === "infractions" && !infractionsLoaded) loadInfractions(); } window.addEventListener("popstate", (e) => { const tab = e.state?.tab || detectTab(); switchTab(tab, false); }); (async function init() { if (!serverId) return; const main = document.getElementById("content"); document.querySelector(".loading")?.remove(); try { const data = await request("GET", `/dash/server/${serverId}`); const s = data.server; perms = s.perms || 0; const cfg = s.serverConfig || {}; const managers = cfg.botManagers || []; const mods = cfg.moderators || []; main.insertAdjacentHTML( "beforeend", `

Server Dashboard

Manage AutoMod's presence in the server ${escHtml(s.name || s.id)}.

${s.iconURL ? `` : ""}

${escHtml(s.name || s.id)}

${s.description ? `

${escHtml(s.description)}

` : ""}
`, ); main.insertAdjacentHTML( "beforeend", `
General Anti-Spam Vote Moderation Infractions Logging
${perms >= 2 ? renderGeneral(cfg, s.defaultPrefix, s.botId) : `

You need Manager permissions to edit the server configuration.

`} ${perms >= 3 ? renderUserSection("managers", "Bot Managers", managers, true) : ""} ${perms >= 2 ? renderUserSection("mods", "Moderators", mods, true) : ""}
Loading anti-spam rules…
${perms >= 2 ? renderVoteModeration(cfg, s.defaultPrefix, s.botId) : `

You need Manager permissions to edit vote moderation settings.

`}
${perms >= 1 ? `
Loading infractions…
` : `

You need Moderator permissions to view infractions.

`}
${perms >= 2 ? renderLogging(cfg, s.channels) : `

You need Manager permissions to configure logging.

`}
`, ); document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", (e) => { e.preventDefault(); switchTab(tab.dataset.tab); }); }); if (perms >= 2) { bindConfigForms(); bindVoteForms(); bindLoggingForms(); } if (perms >= 3) bindUserActions("managers"); if (perms >= 2) bindUserActions("mods"); if (activeTab === "anti-spam") loadAntiSpam(); if (activeTab === "infractions") loadInfractions(); document.getElementById("tab-infractions").addEventListener("click", handleInfractionAction); document.getElementById("tab-infractions").addEventListener("change", handleInfractionChange); } catch (e) { main.insertAdjacentHTML("beforeend", `

Server Settings

${escHtml(e.message)}

Back to Servers`); } })(); function renderGeneral(cfg, defaultPrefix, botId) { const mention = botId ? `<@${botId}>` : "@AutoMod"; return `

General

You can always use ${mention} instead of a prefix. Global default is ${escHtml(defaultPrefix || "/")}. Leave empty to use the default.

Whether a space is required between the prefix and command (eg ?kick vs ? kick).

Sends a direct message to the user when they're kicked, with reason and moderator info.

${desc("Sends a direct message to the user when they're banned, including the reason, moderator info, and ban duration.")}

Sends a direct message to the user when they're warned, with reason and moderator info.

Allows members to check their own infraction history with ${escHtml(cfg.prefix || defaultPrefix || "/")}infractions. Moderators can still view everyone's infractions.

Toggles the automatic spam detection module.

Loading wordlist…
`; } function renderVoteModeration(cfg, defaultPrefix, botId) { const vk = cfg.votekick || {}; const mention = botId ? `<@${botId}>` : "@AutoMod"; return `

Vote Moderation

Allows members to vote to kick, ban, or timeout a user. Use ${escHtml(cfg.prefix || defaultPrefix || "/")}kick vote, ${escHtml(cfg.prefix || defaultPrefix || "/")}ban vote and ${escHtml(cfg.prefix || defaultPrefix || "/")}timeout vote.

Vote Kick

Let members vote to kick a user via ${escHtml(cfg.prefix || defaultPrefix || "/")}kick vote.

Vote Ban

Let members vote to ban a user via ${escHtml(cfg.prefix || defaultPrefix || "/")}ban vote.

Vote Timeout

Let members vote to timeout a user via ${escHtml(cfg.prefix || defaultPrefix || "/")}timeout vote.

`; } function bindConfigForms() { document.getElementById("config-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { clearError(); await request("PUT", `/dash/server/${serverId}/config`, { prefix: document.getElementById("prefix").value || null, spaceAfterPrefix: document.getElementById("spaceAfterPrefix").checked, dmOnKick: document.getElementById("dmOnKick").checked, dmOnWarn: document.getElementById("dmOnWarn").checked, selfInfractions: document.getElementById("selfInfractions").checked, antispamEnabled: document.getElementById("antispamEnabled").checked, dmOnBan: document.getElementById("dmOnBan").checked, }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Configuration"; }); loadWordlist(); } function bindVoteForms() { document.getElementById("votekick-kick-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { clearError(); await request("PUT", `/dash/server/${serverId}/config`, { votekickKickEnabled: document.getElementById("kickEnabled").checked, votekickKickVotesRequired: Number(document.getElementById("kickVotesRequired").value), votekickKickVoteDuration: Number(document.getElementById("kickVoteDuration").value), }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Vote Kick"; }); document.getElementById("votekick-ban-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { const banAction = document.getElementById("banAction").value; const banDuration = banAction === "custom" ? Number(document.getElementById("banDuration").value) : 0; clearError(); await request("PUT", `/dash/server/${serverId}/config`, { votekickBanEnabled: document.getElementById("banEnabled").checked, votekickBanVotesRequired: Number(document.getElementById("banVotesRequired").value), votekickBanVoteDuration: Number(document.getElementById("banVoteDuration").value), votekickBanDuration: banDuration, }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Vote Ban"; }); document.getElementById("votekick-timeout-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { clearError(); await request("PUT", `/dash/server/${serverId}/config`, { votekickTimeoutEnabled: document.getElementById("timeoutEnabled").checked, votekickTimeoutVotesRequired: Number(document.getElementById("timeoutVotesRequired").value), votekickTimeoutVoteDuration: Number(document.getElementById("timeoutVoteDuration").value), votekickTimeoutDuration: Number(document.getElementById("timeoutDuration").value), }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Vote Timeout"; }); document.getElementById("kickEnabled").addEventListener("change", function () { document.querySelector(".vk-kick-options").style.display = this.checked ? "" : "none"; }); document.getElementById("banEnabled").addEventListener("change", function () { document.querySelector(".vk-ban-options").style.display = this.checked ? "" : "none"; }); document.getElementById("timeoutEnabled").addEventListener("change", function () { document.querySelector(".vk-timeout-options").style.display = this.checked ? "" : "none"; }); const vkBanAction = document.getElementById("banAction"); const vkBanDuration = document.getElementById("ban-duration-field"); if (vkBanAction && vkBanDuration) { vkBanAction.addEventListener("change", () => { vkBanDuration.style.display = vkBanAction.value !== "custom" ? "none" : ""; }); } } function renderUserSection(key, title, items, editable) { const list = items.length ? items.map((u) => `
  • ${escHtml(u)}${editable ? `` : ""}
  • `).join("") : `
  • None configured
  • `; return `

    ${title}

    ${ editable ? `
    ` : "" }
    `; } function bindUserActions(key) { document.getElementById(`${key}-list`).addEventListener("click", async (e) => { const btn = e.target.closest(`.remove-${key}`); if (!btn) return; try { clearError(); await request("DELETE", `/dash/server/${serverId}/${key}/${btn.dataset.user}`); window.location.reload(); } catch (err) { showError(err.message); } }); document.getElementById(`add-${key}-form`).addEventListener("submit", async (e) => { e.preventDefault(); const input = document.getElementById(`${key}-input`); const userId = input.value.trim(); if (!userId) return; try { clearError(); await request("PUT", `/dash/server/${serverId}/${key}`, { item: userId }); window.location.reload(); } catch (err) { showError(err.message); } }); } async function loadWordlist() { const section = document.getElementById("wordlist-section"); try { const data = await request("GET", `/dash/server/${serverId}/wordlist`); const words = data.words || []; const action = data.action || {}; const enabled = !!data.enabled; section.innerHTML = `

    Wordlist

    ${ words.length ? `${words .map( (w) => ``, ) .join("")}
    WordStrictnessActions
    ${escHtml(w.word)} ${w.strictness}
    ` : `

    No words configured.

    ` }
    `; bindWordlistEvents(); } catch (e) { section.innerHTML = `

    Wordlist

    Failed to load: ${escHtml(e.message)}

    `; } } function bindWordlistEvents() { document.getElementById("wl-config-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.submitter || e.target.querySelector('button[type="submit"], button:not([type]), button[form]'); btn.disabled = true; try { await request("PUT", `/dash/server/${serverId}/wordlist/config`, { enabled: document.getElementById("wl-enabled").checked, action: document.getElementById("wl-action").value, message: document.getElementById("wl-message").value, }); clearError(); } catch (err) { showError(err.message); } btn.disabled = false; }); document.getElementById("wl-add-form").addEventListener("submit", async (e) => { e.preventDefault(); const word = document.getElementById("wl-new-word").value.trim(); const strictness = document.getElementById("wl-new-strictness").value; if (!word) return; try { clearError(); await request("POST", `/dash/server/${serverId}/wordlist`, { word, strictness }); loadWordlist(); } catch (err) { showError(err.message); } }); document.querySelectorAll(".wl-delete").forEach((btn) => { btn.addEventListener("click", async () => { const word = btn.dataset.word; if (!confirm(`Remove "${word}" from the wordlist?`)) return; try { clearError(); await request("DELETE", `/dash/server/${serverId}/wordlist/${encodeURIComponent(word)}`); loadWordlist(); } catch (err) { showError(err.message); } }); }); } function buildRuleModal({ id, title, formId, formInnerHTML, submitLabel, onSubmit }) { const existing = document.getElementById(id); if (existing) return existing; const modal = document.createElement("dialog"); modal.className = "rule-modal"; modal.id = id; modal.innerHTML = ` `; document.body.appendChild(modal); document.getElementById(formId).addEventListener("submit", onSubmit); document.getElementById(`${id}-cancel`).addEventListener("click", () => modal.close()); return modal; } function getEditModal() { return buildRuleModal({ id: "edit-modal", title: "Edit Rule", formId: "edit-form", submitLabel: "Save", formInnerHTML: `
    `, onSubmit: saveEdit, }); } function getCreateModal() { const modal = buildRuleModal({ id: "create-modal", title: "Create Rule", formId: "create-form", submitLabel: "Create Rule", formInnerHTML: `
    `, onSubmit: createRule, }); return modal; } async function loadAntiSpam() { if (antispamLoaded || antispamLoading) return; antispamLoading = true; const container = document.getElementById("tab-anti-spam"); if (perms < 2) { container.innerHTML = `

    You need Manager permissions to manage anti-spam rules.

    `; antispamLoaded = true; antispamLoading = false; return; } try { const data = await request("GET", `/dash/server/${serverId}/antispam`); rules = data.antispam || []; renderAntiSpam(container); antispamLoaded = true; } catch (e) { container.innerHTML = `

    Failed to load anti-spam rules: ${escHtml(e.message)}

    `; } finally { antispamLoading = false; } } function renderAntiSpam(container) { container.innerHTML = `

    Anti-spam rules allow designating what is considered spam and what actions should be taken when spam is detected. Make sure you've enabled anti-spam in your server for these rules to take effect.

    ${ rules.length ? `${rules .map( (r) => ``, ) .join("")}
    TriggerActionMessage
    ${r.max_msg} messages in ${r.timeframe} second${r.timeframe !== 1 ? "s" : ""} ${ACTIONS[r.action]} ${r.message ? escHtml(r.message) : "N/A"}
    ` : `

    No rules configured.

    ` } `; bindAntiSpamEvents(); } function bindAntiSpamEvents() { document.querySelectorAll(".edit-btn").forEach((btn) => btn.addEventListener("click", () => openEdit(btn.dataset.id))); document.querySelectorAll(".delete-btn").forEach((btn) => btn.addEventListener("click", () => deleteRule(btn.dataset.id))); document.getElementById("create-btn").addEventListener("click", () => getCreateModal().showModal()); } function openEdit(id) { const r = rules.find((r) => r.id == id); if (!r) return; const m = getEditModal(); document.getElementById("edit-id").value = r.id; document.getElementById("edit-max_msg").value = r.max_msg; document.getElementById("edit-timeframe").value = r.timeframe; document.getElementById("edit-action").value = r.action; document.getElementById("edit-message").value = r.message || ""; m.showModal(); } async function saveEdit(e) { e.preventDefault(); const id = document.getElementById("edit-id").value; try { clearError(); await request("PATCH", `/dash/server/${serverId}/antispam/${id}`, { max_msg: Number(document.getElementById("edit-max_msg").value), timeframe: Number(document.getElementById("edit-timeframe").value), action: Number(document.getElementById("edit-action").value), message: document.getElementById("edit-message").value || null, }); antispamLoaded = false; loadAntiSpam(); getEditModal().close(); } catch (err) { showError(err.message); } } async function deleteRule(id) { if (!confirm("Delete this rule?")) return; try { clearError(); await request("DELETE", `/dash/server/${serverId}/antispam/${id}`); antispamLoaded = false; loadAntiSpam(); } catch (err) { showError(err.message); } } async function createRule(e) { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Creating…"; try { clearError(); await request("POST", `/dash/server/${serverId}/antispam`, { max_msg: Number(document.getElementById("cr-max_msg").value), timeframe: Number(document.getElementById("cr-timeframe").value), action: Number(document.getElementById("cr-action").value), message: document.getElementById("cr-message").value || null, }); antispamLoaded = false; loadAntiSpam(); getCreateModal().close(); e.target.reset(); btn.disabled = false; btn.textContent = "Create Rule"; } catch (err) { showError(err.message); btn.disabled = false; btn.textContent = "Create Rule"; } } async function loadInfractions() { if (infractionsLoading) return; if (perms < 1) { const container = document.getElementById("tab-infractions"); container.innerHTML = `

    You need Moderator permissions to view infractions.

    `; return; } if (infObs) { infObs.disconnect(); infObs = null; } infractionsLoading = true; infractionsLoaded = false; const container = document.getElementById("tab-infractions"); try { const data = await request("GET", `/dash/server/${serverId}/infractions?limit=50`); renderInfractions(container, data); infractionsLoaded = true; } catch (e) { container.innerHTML = `

    Failed to load infractions: ${escHtml(e.message)}

    `; } finally { infractionsLoading = false; } } function renderInfractions(container, data, append = false) { const items = data.infractions || []; const total = data.total || 0; const hasMore = data.hasMore || false; const stats = data.stats || {}; if (!append) { container.innerHTML = `
    ${stats.total || 0}Total
    ${stats.warns || 0}Warns
    ${stats.kicks || 0}Kicks
    ${stats.bans || 0}Bans
    ${stats.timeouts || 0}Timeouts

    Infractions

    Showing ${items.length} of ${total} infractions.

    ${renderInfractionTable(items)}`; } else { const table = container.querySelector("table tbody"); if (table) { table.insertAdjacentHTML("beforeend", buildInfractionRows(items)); } const selectAll = document.getElementById("inf-select-all"); if (selectAll) selectAll.checked = false; if (hasMore) { container.insertAdjacentHTML("beforeend", `
    `); } } if (hasMore && !append) { container.insertAdjacentHTML("beforeend", `
    `); } bindInfractionEvents(); if (!append) resolvedUserIds = new Set(); resolveInfractionUsers(); } function renderInfractionTable(items) { return ` ${perms >= 2 ? `` : ""} ${perms >= 2 ? `` : ""} ${items.length ? buildInfractionRows(items) : ``}
    DateUserTypeModeratorReason
    No infractions found.
    `; } function buildInfractionRows(items) { return items .map( (i) => ` ${perms >= 2 ? `` : ""} ${new Date(i.date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })} ${i.userName ? `${escHtml(i.userName)}` : `${escHtml(i.user)}`} ${i.actionType} ${i.createdByName ? `${escHtml(i.createdByName)}` : i.createdBy ? `${escHtml(i.createdBy)}` : "AutoMod"} ${escHtml(i.reason || "No reason provided")} ${perms >= 2 ? `` : ""} ${perms >= 2 && i.actionType === "ban" ? `` : ""} `, ) .join(""); } let infObs = null; async function resolveInfractionUsers() { if (infractionsResolving) return; const container = document.getElementById("tab-infractions"); const userCells = container.querySelectorAll("[data-resolve-user]"); const banButtons = container.querySelectorAll("[data-resolve-ban]"); const idsToResolve = new Set(); for (const cell of userCells) { const id = cell.dataset.resolveUser; if (id && !resolvedUserIds.has(id)) idsToResolve.add(id); } for (const btn of banButtons) { const id = btn.dataset.resolveBan; if (id && !resolvedUserIds.has(id)) idsToResolve.add(id); } if (idsToResolve.size === 0) return; infractionsResolving = true; try { const data = await request("POST", `/dash/server/${serverId}/infractions/resolve`, { userIds: [...idsToResolve], }); const users = data.users || {}; const bannedIds = new Set(data.bannedIds || []); for (const cell of userCells) { const id = cell.dataset.resolveUser; if (!id || !users[id] || !users[id].username) continue; cell.innerHTML = `${escHtml(users[id].username)}`; cell.removeAttribute("data-resolve-user"); resolvedUserIds.add(id); } for (const btn of banButtons) { const id = btn.dataset.resolveBan; if (!id) continue; if (bannedIds.has(id)) { btn.hidden = false; } else { btn.remove(); } resolvedUserIds.add(id); } } catch { /* names will remain as IDs */ } finally { infractionsResolving = false; } } async function handleInfractionAction(e) { const delBtn = e.target.closest(".inf-delete"); const unbanBtn = e.target.closest(".inf-unban"); const bulkBtn = e.target.closest("#inf-bulk-delete"); if (delBtn) { if (!confirm("Delete this infraction record?")) return; delBtn.disabled = true; delBtn.textContent = "…"; try { clearError(); await request("DELETE", `/dash/server/${serverId}/infractions/${delBtn.dataset.id}`); loadInfractions(); } catch (err) { showError(err.message); delBtn.disabled = false; delBtn.textContent = "Delete"; } } if (unbanBtn) { if (!confirm("Unban user " + unbanBtn.dataset.user + "?")) return; unbanBtn.disabled = true; unbanBtn.textContent = "…"; try { clearError(); await request("POST", `/dash/server/${serverId}/unban`, { target: unbanBtn.dataset.user }); loadInfractions(); } catch (err) { showError(err.message); unbanBtn.disabled = false; unbanBtn.textContent = "Unban"; } } if (bulkBtn) { const checked = document.querySelectorAll(".inf-checkbox:checked"); if (!checked.length) return; if (!confirm(`Delete ${checked.length} infraction record${checked.length > 1 ? "s" : ""}?`)) return; bulkBtn.disabled = true; bulkBtn.textContent = "Deleting…"; try { clearError(); const ids = Array.from(checked).map((c) => c.dataset.id); await request("POST", `/dash/server/${serverId}/infractions/bulk-delete`, { ids }); loadInfractions(); } catch (err) { showError(err.message); bulkBtn.disabled = false; bulkBtn.textContent = "Delete Selected"; } } } function updateSelectionBar() { const all = document.querySelectorAll(".inf-checkbox"); const checked = document.querySelectorAll(".inf-checkbox:checked"); const bar = document.getElementById("inf-selection-bar"); const count = document.getElementById("inf-selection-count"); const selectAll = document.getElementById("inf-select-all"); if (!bar || !count) return; if (checked.length > 0) { bar.hidden = false; count.textContent = `${checked.length} selected`; } else { bar.hidden = true; } if (selectAll) { selectAll.checked = all.length > 0 && checked.length === all.length; selectAll.indeterminate = checked.length > 0 && checked.length < all.length; } } function handleInfractionChange(e) { if (e.target.id === "inf-select-all") { const all = document.querySelectorAll(".inf-checkbox"); all.forEach((cb) => { cb.checked = e.target.checked; }); } updateSelectionBar(); } function getCheckedActions() { return Array.from(document.querySelectorAll(".inf-action-check:checked")) .map((c) => c.value) .join(","); } function bindInfractionEvents() { document.getElementById("infraction-filter-form").addEventListener("submit", async (e) => { e.preventDefault(); const search = document.getElementById("inf-search").value.trim(); const action = getCheckedActions(); const params = new URLSearchParams({ limit: "50" }); if (search) params.set("search", search); if (action) params.set("action", action); try { clearError(); const data = await request("GET", `/dash/server/${serverId}/infractions?${params.toString()}`); const container = document.getElementById("tab-infractions"); if (infObs) infObs.disconnect(); renderInfractions(container, data, false); } catch (err) { showError(err.message); } }); const container = document.getElementById("tab-infractions"); const sentinel = document.getElementById("inf-sentinel"); if (sentinel) { if (infObs) infObs.disconnect(); infObs = new IntersectionObserver( async (entries) => { if (!entries[0].isIntersecting) return; infObs.disconnect(); const search = document.getElementById("inf-search").value.trim(); const action = getCheckedActions(); const rows = container.querySelectorAll("table tbody tr"); const lastRow = rows.length ? rows[rows.length - 1] : null; const before = lastRow ? lastRow.dataset.date || "" : ""; const params = new URLSearchParams({ limit: "50" }); if (search) params.set("search", search); if (action) params.set("action", action); if (before) params.set("before", before); try { const data = await request("GET", `/dash/server/${serverId}/infractions?${params.toString()}`); renderInfractions(container, data, true); } catch (err) { showError(err.message); } }, { rootMargin: "200px" }, ); infObs.observe(sentinel); } } function renderLogging(cfg, channels) { const logs = cfg.logs || {}; const mu = logs.messageUpdate?.stoat || {}; const ma = logs.modAction?.stoat || {}; const channelOptions = `` + channels.map((c) => ``).join(""); const typeOptions = (selected) => ` `; return `

    Message Logs

    Logs when messages are edited or deleted.

    Mod Action Logs

    Logs moderation actions (warns, kicks, bans, timeouts).

    `; } function bindLoggingForms() { document.getElementById("log-messageupdate-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { clearError(); const channel = document.getElementById("mu-channel").value; const type = document.getElementById("mu-type").value; await request("PUT", `/dash/server/${serverId}/logs`, { messageUpdate: channel ? { channel, type } : null, }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Message Logs"; }); document.getElementById("log-modaction-form").addEventListener("submit", async (e) => { e.preventDefault(); const btn = e.target.querySelector("button"); btn.disabled = true; btn.textContent = "Saving…"; try { clearError(); const channel = document.getElementById("ma-channel").value; const type = document.getElementById("ma-type").value; await request("PUT", `/dash/server/${serverId}/logs`, { modAction: channel ? { channel, type } : null, }); } catch (err) { showError(err.message); } btn.disabled = false; btn.textContent = "Save Mod Action Logs"; }); }