// Staff panel: hash-routed pages over the /api/admin endpoints. Buttons only appear for what the // signed-in rank may do, but the server checks every action again. const PAGES = [ { id: "overview", label: "Overview" }, { id: "players", label: "Players" }, { id: "bans", label: "Bans" }, { id: "comms", label: "Gags & mutes" }, { id: "staff", label: "Staff & ranks" }, { id: "settings", label: "Server settings" }, ]; const state = { page: "overview", banFilter: "active", commFilter: "active", playerFilter: "online", query: "", num: 1, group: null, me: null, counts: {}, }; const can = (flag) => !!state.me && (state.me.perms.includes("@css/root") || state.me.perms.includes(flag)); /* ---------- Overview ---------- */ const ACTIVITY_VERB = { ban: "banned", warn: "warned", unban: "unbanned", comm: { GAG: "gagged", MUTE: "muted", SILENCE: "silenced" }, unmute: { GAG: "lifted the gag on", MUTE: "lifted the mute on", SILENCE: "lifted the silence on" }, }; const ACTIVITY_COLOR = { ban: "var(--accent)", comm: "var(--amber)", warn: "var(--faint)", unban: "var(--green)", unmute: "var(--green)" }; function activityLine(a) { const verb = typeof ACTIVITY_VERB[a.kind] === "object" ? ACTIVITY_VERB[a.kind][a.type] : ACTIVITY_VERB[a.kind]; const len = (a.kind === "ban" || a.kind === "comm") ? (a.duration === 0 ? " permanently" : ` for ${fmtDuration(a.duration)}`) : ""; return `
  • ${esc(a.admin)} ${verb} ${esc(a.name || a.steamid)}${len}

    ${fmtAgo(a.at)}${a.reason ? `, ${esc(a.reason)}` : ""}
  • `; } function liveHeader(s) { if (!s.online) { return `

    The panel can't reach the game server, so live status and in-game actions are off. Bans and blocks still save to the database.

    Offline
    `; } return `

    ${esc(s.hostname)}${s.warmup ? ", warmup" : ""}

    ${esc(s.map)}
    ${s.count}/${s.maxPlayers}Players
    ${s.scoreT} : ${s.scoreCt}${s.warmup ? "Warmup, no rounds yet" : "Rounds won, T vs CT"}
    ${s.staffOnline}Staff online
    `; } function tagsFor(p) { return (p.tags || []).map((t) => { if (t.startsWith("WARN:")) { const n = t.slice(5); return `${esc(n)} warn${n === "1" ? "" : "s"}`; } return `${COMM_LABEL[t]}`; }).join(" "); } function onlineTable(players) { const list = players.filter((p) => !p.bot).sort((a, b) => (a.team === b.team ? b.kills - a.kills : b.team - a.team)); if (!list.length) return '
    Nobody is on the server.
    '; const teamName = { 2: "T", 3: "CT" }; return ` ${list.map((p) => ``).join("")}
    PlayerRankK / DPingActions
    ${teamName[p.team] || "SPEC"}
    ${esc(p.name)} ${tagsFor(p)}
    ${rankTag(p.rank)} ${p.kills} / ${p.deaths} ${p.ping}
    ${p.canTarget ? ` ${can("@css/chat") ? `` : ""} ${can("@css/kick") ? `` : ""} ${can("@css/ban") ? `` : ""}` : ""}
    `; } async function overview() { const v = $("#view"); let d; try { d = await api("/api/admin/overview"); } catch (err) { v.innerHTML = errorBox(err); return; } if (state.page !== "overview") return; state.counts = { bans: d.activeBans, comms: d.activeComms, staff: d.staffCount, players: d.server.online ? `${d.server.count}/${d.server.maxPlayers}` : "" }; renderNav(); setServerPick(d.server); const s = d.server; v.innerHTML = ` ${liveHeader(s)}

    On the server

    ${can("@css/changemap") ? '' : ""} ${can("@css/chat") ? '' : ""}
    ${s.online ? onlineTable(s.players) : '
    Offline.
    '}

    Recent actions

    All bans
    ${d.activity.length ? `
      ${d.activity.map(activityLine).join("")}
    ` : '
    Nothing yet.
    '}

    Ending soon

    ${d.activeBans} bans, ${d.activeComms} blocks active

    ${d.expiring.length ? `
      ${d.expiring.map((p) => `
    • ${esc(p.name || p.steamid)} ${p.kind === "ban" ? "ban" : COMM_LABEL[p.type].toLowerCase()}${fmtLeft(served(p).left)}
    • `).join("")}
    ` : '
    No timed bans or blocks.
    '}
    `; } function setServerPick(s) { const el = $("#server-pick"); el.innerHTML = s.online ? `${esc(s.hostname)}${esc(s.map)}, ${s.count}/${s.maxPlayers} players` : `Server offlineLive status unavailable`; } /* ---------- Players ---------- */ function players() { $("#view").innerHTML = `

    Players

    Everyone SimpleAdmin has seen join, with their rank and record. Search by name, SteamID64${can("@css/showip") ? " or IP address" : ""}.

    ${searchBox("Search players", state.query)} ${segmented("playerFilter", state.playerFilter, [["online", "Online"], ["ranked", "Ranked"], ["all", "All"]])}
    ${loading()}
    `; loadResults(); } function playersTable(list) { if (!list.items.length) { if (state.query) return `
    No players match "${esc(state.query)}". Try a SteamID64 or part of a name.
    `; return `
    ${state.playerFilter === "online" ? "Nobody is on the server." : "No players yet."}
    `; } return ` ${list.items.map((p) => ``).join("")}
    PlayerRankRecordLast seen
    ${initial(p.name)}
    ${esc(p.name || p.steamid)}${esc(p.steamid)}
    ${rankTag(p.rank)} ${p.banned ? 'Banned' : ""} ${p.bans ? `${p.bans} ban${p.bans > 1 ? "s" : ""}` : ""} ${p.comms ? `${p.comms} block${p.comms > 1 ? "s" : ""}` : ""} ${p.warns ? `${p.warns} warn${p.warns > 1 ? "s" : ""}` : ""} ${!p.bans && !p.comms && !p.warns ? 'Clean' : ""} ${p.online ? 'Online' : p.lastSeen ? `${fmtAgo(p.lastSeen)}` : 'Never joined'}
    ${pager(list)}`; } /* ---------- Bans and comms ---------- */ function punishTable(list, kind) { if (!list.items.length) { return `
    ${state.query ? `Nothing matches "${esc(state.query)}".` : kind === "ban" ? "No bans in this view." : "No gags or mutes in this view."}
    `; } const canLift = kind === "ban" ? can("@css/unban") : can("@css/chat"); const canAdd = kind === "ban" ? can("@css/ban") : can("@css/chat"); return ` ${kind === "comm" ? "" : ""} ${list.items.map((p) => ` ${kind === "comm" ? `` : ""} `).join("")}
    PlayerTypeReason Length Issued Status Actions
    ${p.steamid ? `${esc(p.name || p.steamid)}` : esc(p.name || "Unknown")}${esc(p.steamid)}${p.ip ? `
    ${esc(p.ip)}` : ""}
    ${COMM_LABEL[p.type]}${esc(p.reason)}${p.liftedBy || p.liftReason ? `
    Lifted${p.liftedBy ? ` by ${esc(p.liftedBy)}` : ""}${p.liftReason ? `: ${esc(p.liftReason)}` : ""}` : ""}
    ${termCell(p)} ${fmtDate(p.created)}
    by ${esc(p.admin || "Console")}
    ${statusTag(p)}
    ${p.status === "active" && canLift && p.steamid ? `` : ""} ${p.status !== "active" && canAdd && p.steamid ? `` : ""}
    ${pager(list)}`; } function bans() { $("#view").innerHTML = `

    Bans

    The bar under each length shows how much of the ban has been served. Striped bars never expire.

    ${can("@css/ban") ? `` : ""}
    ${searchBox(`Search by name, SteamID, ${can("@css/showip") ? "IP, " : ""}reason or admin`, state.query)} ${segmented("banFilter", state.banFilter, [["active", "Active"], ["expired", "Expired"], ["lifted", "Lifted"], ["all", "All"]])}
    ${loading()}
    `; loadResults(); } function comms() { $("#view").innerHTML = `

    Gags & mutes

    A gag blocks text chat, a mute blocks voice, and a silence blocks both.

    ${can("@css/chat") ? `` : ""}
    ${searchBox("Search by name, SteamID, reason or admin", state.query)} ${segmented("commFilter", state.commFilter, [["active", "Active"], ["GAG", "Gags"], ["MUTE", "Mutes"], ["SILENCE", "Silences"], ["expired", "Expired"], ["all", "All"]])}
    ${loading()}
    `; loadResults(); } async function loadResults() { const box = $("#results"); if (!box) return; let path, status, render; if (state.page === "players") { path = "/api/admin/players"; render = playersTable; } else if (state.page === "bans") { path = "/api/admin/bans"; status = state.banFilter; render = (l) => punishTable(l, "ban"); } else { path = "/api/admin/comms"; status = state.commFilter; render = (l) => punishTable(l, "comm"); } const params = { status, q: state.query, page: state.num, filter: state.page === "players" ? state.playerFilter : undefined }; const want = JSON.stringify([state.page, params]); loadResults.want = want; try { const list = await api(path + qs(params)); if (loadResults.want !== want) return; box.innerHTML = render(list); } catch (err) { box.innerHTML = errorBox(err); } } /* ---------- Staff & ranks ---------- */ async function staff() { const v = $("#view"); let d; try { d = await api("/api/admin/staff"); } catch (err) { v.innerHTML = errorBox(err); return; } if (state.page !== "staff") return; staff.data = d; const manage = d.canManage; if (!d.groups.some((g) => g.id === state.group)) state.group = d.groups[0]?.id ?? null; const g = d.groups.find((x) => x.id === state.group); const ladderItem = (x) => `
  • `; const staffGroups = d.groups.filter((x) => !x.supporter); const supporters = d.groups.filter((x) => x.supporter); const memberRow = (p, groupName) => `
    ${initial(p.name)}
    ${esc(p.name)}${esc(p.steamid)}
    ${p.online ? 'Online' : p.lastSeen ? `Seen ${fmtAgo(p.lastSeen)}` : 'Never joined'} ${p.ends ? `Until ${fmtDate(p.ends)}` : "No expiry"}
    ${manage && p.steamid !== d.me ? `` : ""}
    `; const allFlags = [...d.flags]; if (g) for (const f of g.flags) if (!allFlags.some((x) => x.flag === f)) allFlags.push({ flag: f, label: f, help: "Custom permission" }); const root = g && g.flags.includes("@css/root"); v.innerHTML = `

    Staff & ranks

    Ranks are listed by immunity. Staff can only act on players whose immunity isn't higher than their own.${manage ? "" : " Only Root can change ranks and staff."}

    ${manage ? `
    ` : ""}
    ${d.groups.length ? `

    Ranks

    Immunity

    ${esc(g.label)}

    ${esc(g.name)}

    Permissions

    ${allFlags.map((f) => { const on = g.flags.includes(f.flag); const inherited = root && f.flag !== "@css/root"; return ``; }).join("")}
    ${manage ? `
    ` : ""}

    Members

    ${g.members.length} ${g.members.length === 1 ? "person" : "people"}

    ${g.members.length ? `${g.members.map((p) => memberRow(p, g.name)).join("")}
    ` : `
    Nobody has this rank yet.${manage ? " Use Add staff to give it to someone." : ""}
    `}
    ${d.unranked.length ? `

    Admins without a rank

    Permissions set directly in SimpleAdmin

    ${d.unranked.map((p) => memberRow(p, "")).join("")}
    ` : ""}
    ` : `
    SimpleAdmin has no ranks for this server yet.${manage ? " Use New rank to create one." : ""}
    `}`; } /* ---------- Settings ---------- */ async function settings() { const v = $("#view"); let d; try { d = await api("/api/admin/settings"); } catch (err) { v.innerHTML = errorBox(err); return; } if (state.page !== "settings") return; const sa = d.simpleadmin; const cvarControl = (c) => { if (c.type === "bool") return ``; if (c.type === "int") return ``; return ``; }; const saControl = (s) => { const val = sa.values[s.key]; const dis = d.canRoot ? "" : "disabled"; if (s.type === "bool") return ``; if (s.type === "select") return ``; return ``; }; const row = (label, help, key, control) => `
    ${esc(label)}${help ? `

    ${esc(help)}

    ` : ""}${key ? `${esc(key)}` : ""}
    ${control}
    `; v.innerHTML = `

    Server settings

    Server changes go live over RCON. SimpleAdmin's settings are saved to its config file.

    Server

    Live over RCON. Resets when the server restarts.

    ${!d.canCvar ? '
    Your rank can\'t change server settings.
    ' : !d.server.online ? '
    The game server isn\'t answering, so these can\'t be read or changed right now.
    ' : (d.cvars || []).map((c) => row(c.label, c.help, c.key, cvarControl(c))).join("")}

    Punishments

    ${sa.available ? `${sa.path ? `Saved to ${esc(sa.path)}. ` : ""}Applies after the next server restart.` : "Read-only"}

    ${!sa.available ? '
    The panel wasn\'t given CS2-SimpleAdmin.json (SAW_SA_CONFIG), so these can\'t be shown or changed here.
    ' : sa.error ? `
    ${esc(sa.error)}
    ` : sa.settings.map((s) => row(s.label, s.help, s.key, saControl(s))).join("")}

    Connections

    ${row("Database", "SimpleAdmin's MySQL database.", "", d.database.connected ? 'Connected' : 'Not connected')} ${row("Game server", d.server.online ? "Live status through the WebPanelBridge plugin." : d.server.error || "", "", d.server.online ? 'Connected' : 'Not answering')}

    Maintenance

    Each of these asks first.

    ${can("@css/root") ? row("Reload admins", "Re-read ranks and staff from the database. Runs css_reloadadmins.", "", '') : ""} ${can("@css/generic") ? row("Restart the match", "Restarts the game on the current map. Runs css_rr.", "", '') : ""} ${can("@css/changemap") ? row("Change map", "Switch to another map, or a Workshop map with ws:.", "", '') : ""}
    `; } function markDirty() { const b = $("#savebar"); if (b) b.hidden = false; } async function saveSettings() { const cvars = {}; const sa = {}; document.querySelectorAll("[data-cvar].dirty").forEach((el) => { const key = el.dataset.cvar; if (el.classList.contains("switch")) cvars[key] = el.getAttribute("aria-checked") === "true"; else cvars[key] = el.type === "number" ? Number(el.value) : el.value; }); document.querySelectorAll("[data-sa].dirty").forEach((el) => { const key = el.dataset.sa; if (el.classList.contains("switch")) sa[key] = el.getAttribute("aria-checked") === "true"; else sa[key] = Number(el.value); }); const msgs = []; try { if (Object.keys(cvars).length) msgs.push((await api("/api/admin/settings/cvars", { method: "POST", body: cvars })).message); if (Object.keys(sa).length) msgs.push((await api("/api/admin/settings/simpleadmin", { method: "POST", body: sa })).message); } catch (err) { toast(err.message); return; } toast(msgs.join(" ") || "Nothing to save."); settings(); } /* ---------- Player drawer ---------- */ async function openPlayer(sid) { $("#drawer-root").innerHTML = `
    `; let d; try { d = await api(`/api/admin/players/${encodeURIComponent(sid)}`); } catch (err) { $(".drawer").innerHTML = errorBox(err); return; } const p = d.player; const name = p.name || p.steamid; const history = [ ...d.bans.map((b) => ({ ...b, label: "Ban" })), ...d.comms.map((c) => ({ ...c, label: COMM_LABEL[c.type] })), ...d.warns.map((w) => ({ ...w, label: "Warning" })), ].sort((a, b) => new Date(b.created) - new Date(a.created)); const t = d.canTarget; const btn = (act, label, perm, extra = "", cls = "") => can(perm) ? `` : ""; const manage = can("@css/root") && p.steamid !== state.me.steamid; $("#drawer-root").innerHTML = `
    `; $(".drawer .close").focus(); } /* ---------- Dialogs ---------- */ const DURATIONS = [[30, "30 min"], [60, "1 hour"], [1440, "1 day"], [10080, "1 week"], [43200, "30 days"], [0, "Permanent"]]; const REASONS = { ban: ["Cheating", "Ban evasion", "Toxicity", "Griefing", "Exploiting"], comm: ["Spam", "Toxicity", "Mic spam", "Soundboard", "Advertising"], warn: ["Toxicity", "Spam", "Griefing", "Ignoring staff"], }; function punishDialog(kind, { sid = "", name = "", type = "GAG", reason = "" } = {}) { const title = { ban: "Ban player", comm: "Gag or mute player", warn: "Warn player" }[kind]; const submit = { ban: "Ban player", comm: "Apply block", warn: "Warn player" }[kind]; const perm = kind === "ban" ? "@css/permban" : "@css/permmute"; openModal(`
    `); } async function rankDialog(sid, name, current) { if (!staff.data) { try { staff.data = await api("/api/admin/staff"); } catch (err) { toast(err.message); return; } } const groups = staff.data.groups; const opts = (groups || []).map((g) => ``).join(""); openModal(`
    `); } function newGroupDialog() { openModal(`
    `); } function textDialog(kind) { const cfg = { map: { title: "Change map", label: "Map", placeholder: "de_mirage, or ws:", submit: "Change map", pattern: "(ws:)?[A-Za-z0-9_\\-]{1,64}" }, say: { title: "Message everyone", label: "Message", placeholder: "Shown in chat to everyone on the server", submit: "Send", pattern: ".{1,200}" }, kick: { title: "Kick player", label: "Reason", placeholder: "Shown to the player", submit: "Kick", pattern: ".{0,200}" }, }[kind]; return cfg; } function inputDialog(kind, data = {}) { const c = textDialog(kind); openModal(`
    `); } function confirmDialog(title, body, label, run) { confirmDialog.run = run; openModal(`
    `); } function openModal(html) { const m = $("#modal"); m.innerHTML = html; m.showModal(); } // act runs an API call, shows its message, and refreshes the page and any open drawer. async function act(path, method, body) { try { const res = await api(path, { method, body }); toast(res.message || "Done."); } catch (err) { toast(err.message); return false; } const drawerSid = $(".drawer [data-sid]")?.dataset.sid; route(); if (drawerSid) openPlayer(drawerSid); return true; } /* ---------- Routing & events ---------- */ const RENDER = { overview, players, bans, comms, staff, settings }; function renderNav() { const c = state.counts; const count = { players: c.players, bans: c.bans, comms: c.comms, staff: c.staff }; $("#nav-links").innerHTML = PAGES.map((p) => `
  • ${p.label}${count[p.id] !== undefined && count[p.id] !== "" ? `${esc(count[p.id])}` : ""}
  • `).join(""); } function route() { const page = location.hash.slice(1); state.page = RENDER[page] ? page : "overview"; renderNav(); $("#nav").classList.remove("open"); $("#menu-btn").setAttribute("aria-expanded", "false"); if (!$("#view").innerHTML) $("#view").innerHTML = loading(); RENDER[state.page](); } window.addEventListener("hashchange", () => { state.query = ""; state.num = 1; $("#view").innerHTML = loading(); route(); window.scrollTo(0, 0); }); const search = debounce(() => loadResults(), 250); document.addEventListener("input", (e) => { const t = e.target; if (t.id === "q") { state.query = t.value; state.num = 1; search(); return; } if (t.matches("[data-cvar], [data-sa]")) { t.classList.add("dirty"); markDirty(); } }); document.addEventListener("change", (e) => { if (e.target.matches("select[data-sa]")) { e.target.classList.add("dirty"); markDirty(); } }); document.addEventListener("click", (e) => { const t = e.target; if (t.closest("#menu-btn")) { const open = $("#nav").classList.toggle("open"); $("#menu-btn").setAttribute("aria-expanded", String(open)); return; } if (t.closest("#signout")) { api("/auth/logout", { method: "POST" }).finally(() => { location.href = "/"; }); return; } const seg = t.closest("[data-seg] button"); if (seg) { const group = seg.parentElement; group.querySelectorAll("button").forEach((b) => b.setAttribute("aria-pressed", String(b === seg))); const key = group.dataset.seg; if (key in state) { state[key] = seg.dataset.value; state.num = 1; loadResults(); } return; } const pg = t.closest("[data-page]"); if (pg && !pg.disabled) { state.num = Number(pg.dataset.page); loadResults(); return; } const chip = t.closest("[data-chips] .chip-btn"); if (chip) { const box = chip.parentElement; if (box.dataset.chips === "reason") $("#reason").value = chip.dataset.value; box.querySelectorAll(".chip-btn").forEach((b) => b.setAttribute("aria-pressed", String(b === chip))); return; } const sw = t.closest(".switch"); if (sw && !sw.disabled) { sw.setAttribute("aria-checked", String(sw.getAttribute("aria-checked") !== "true")); sw.classList.add("dirty"); markDirty(); return; } const grp = t.closest("[data-group]:not([data-act])"); if (grp) { state.group = Number(grp.dataset.group); staff(); return; } if (t.closest("[data-close]")) { closeDrawer(); return; } if (t.closest("[data-modal-close]")) { $("#modal").close(); return; } const a = t.closest("[data-act]"); if (a) { e.preventDefault(); e.stopPropagation(); if (a.disabled) return; const d = a.dataset; switch (d.act) { case "ban": return punishDialog("ban", { sid: d.sid, name: d.name, reason: d.reason }); case "comm": return punishDialog("comm", { sid: d.sid, name: d.name, type: d.type, reason: d.reason }); case "warn": return punishDialog("warn", { sid: d.sid, name: d.name }); case "kick": return inputDialog("kick", { sid: d.sid, name: d.name }); case "map": return inputDialog("map"); case "say": return inputDialog("say"); case "lift": { const what = d.kind === "ban" ? "Unban" : `Lift ${COMM_LABEL[d.type].toLowerCase()}`; confirmDialog.reason = true; return confirmDialog(what, d.kind === "ban" ? `Unban ${esc(d.name || "this player")}? They can join again straight away. This lifts every active ban on their SteamID.` : `Lift the ${COMM_LABEL[d.type].toLowerCase()} on ${esc(d.name || "this player")}?`, d.kind === "ban" ? "Unban" : "Lift", (form) => act("/api/admin/penalties/lift", "POST", { kind: d.kind, id: Number(d.id), reason: form.reason?.value || "" })); } case "rank": return rankDialog(d.sid, d.name, d.group); case "add-staff": return rankDialog("", "", ""); case "new-group": return newGroupDialog(); case "delete-group": confirmDialog.reason = false; return confirmDialog("Delete rank", `Delete ${esc(d.name)}? Its members become regular players.`, "Delete rank", () => act(`/api/admin/groups/${d.id}`, "DELETE")); case "reload-admins": confirmDialog.reason = false; return confirmDialog("Reload admins", "Re-read all ranks and staff from the database?", "Reload admins", () => act("/api/admin/reload-admins", "POST", {})); case "restart": confirmDialog.reason = false; return confirmDialog("Restart the match", "Restart the game on the current map? The current match ends.", "Restart match", () => act("/api/admin/restart", "POST", {})); case "save-settings": return saveSettings(); case "discard": return settings(); } return; } const row = t.closest("[data-player]"); if (row) { e.preventDefault(); openPlayer(row.dataset.player); } }); document.addEventListener("submit", async (e) => { const form = e.target; e.preventDefault(); const kind = form.dataset.submit; const f = form.elements; if (form.id === "group-form") { const flags = [...form.querySelectorAll('input[name="flag"]')].filter((i) => i.checked && (!i.disabled || i.dataset.kept)).map((i) => i.value); await act(`/api/admin/groups/${form.dataset.id}`, "PUT", { name: f.name.value.trim(), immunity: Number(f.immunity.value), flags }); return; } $("#modal").close(); switch (kind) { case "penalty": { const dur = form.querySelector('[data-chips="dur"] [aria-pressed="true"]'); const type = form.querySelector('[data-seg="commType"] [aria-pressed="true"]')?.dataset.value; await act("/api/admin/penalties", "POST", { kind: form.dataset.kind, type: type || "", steamid: f.steamid.value.trim(), duration: Number(dur?.dataset.value ?? 60), reason: f.reason.value, }); break; } case "rank": { const sid = f.steamid.value.trim(); const group = f.group.value; if (form.dataset.had) await act(`/api/admin/staff/${encodeURIComponent(sid)}`, "PUT", { group, days: Number(f.days.value) }); else await act("/api/admin/staff", "POST", { steamid: sid, group, days: Number(f.days.value) }); break; } case "new-group": if (await act("/api/admin/groups", "POST", { name: f.name.value.trim(), immunity: Number(f.immunity.value), flags: ["@css/generic"] })) { state.group = null; } break; case "map": await act("/api/admin/map", "POST", { map: f.value.value.trim() }); break; case "say": await act("/api/admin/say", "POST", { message: f.value.value }); break; case "kick": await act("/api/admin/kick", "POST", { steamid: form.dataset.sid, reason: f.value.value }); break; case "confirm": await confirmDialog.run?.(f); break; } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("#drawer-root").innerHTML && !$("#modal").open) closeDrawer(); }); (async () => { try { state.me = await api("/api/me"); } catch (err) { $("#view").innerHTML = errorBox(err); return; } if (!state.me.staff) { location.href = "/admin"; return; } const name = state.me.site; const i = name.indexOf("."); document.querySelectorAll(".site-name").forEach((el) => { el.innerHTML = i > 0 ? `${esc(name.slice(0, i))}.${esc(name.slice(i + 1))}` : esc(name); }); $("#me").innerHTML = `${initial(state.me.name)}
    ${esc(state.me.name)}${esc(state.me.rank?.label || "Staff")}
    `; // Fill the sidebar counts once, even when starting on another page. api("/api/admin/overview").then((d) => { state.counts = { bans: d.activeBans, comms: d.activeComms, staff: d.staffCount, players: d.server.online ? `${d.server.count}/${d.server.maxPlayers}` : "" }; setServerPick(d.server); renderNav(); }).catch(() => {}); route(); })();