// Public, read-only site: the live server, bans, gags/mutes and staff. The API leaves out admin // names and IPs; nothing here can change anything. const PAGES = [ { id: "server", label: "Server" }, { id: "bans", label: "Bans" }, { id: "comms", label: "Gags & mutes" }, { id: "staff", label: "Staff" }, ]; const state = { page: "server", banFilter: "active", commFilter: "active", query: "", num: 1 }; function siteName(title) { const i = title.indexOf("."); return i > 0 ? `${esc(title.slice(0, i))}.${esc(title.slice(i + 1))}` : esc(title); } /* ---------- Server ---------- */ async function server() { const v = $("#view"); let s; try { s = await api("/api/public/server"); } catch (err) { v.innerHTML = errorBox(err); return; } if (state.page !== "server") return; if (!s.online) { v.innerHTML = `

The server is offline or restarting.

Offline
`; return; } const side = (team, name, score) => { const list = s.players.filter((p) => p.team === team).sort((a, b) => b.kills - a.kills); const humans = list.filter((p) => !p.bot).length; return `

${name}

${humans} ${humans === 1 ? "player" : "players"}${list.length > humans ? `, ${list.length - humans} ${list.length - humans === 1 ? "bot" : "bots"}` : ""}

${s.warmup ? 'Warmup' : `
${score}${score === 1 ? "round" : "rounds"} won
`}
${list.length ? ` ${list.map((p) => ``).join("")}
PlayerKillsDeathsPing
${esc(p.name)}${p.bot ? 'Bot' : p.rank ? rankTag(p.rank) : ""}
${p.kills} ${p.deaths} ${p.bot ? "" : p.ping}
` : '
Nobody on this team.
'}
`; }; const spectators = s.players.filter((p) => p.team !== 2 && p.team !== 3 && !p.bot); v.innerHTML = `

${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
${s.address ? `
Join server or type connect ${esc(s.address)} in the console
` : ""}
${side(3, "Counter-Terrorists", s.scoreCt)} ${side(2, "Terrorists", s.scoreT)}
${spectators.length ? `

Spectating: ${spectators.map((p) => esc(p.name)).join(", ")}

` : ""}`; } /* ---------- Bans and comms ---------- */ function publicTable(rows, kind) { if (!rows.length) { return `
${state.query ? `Nobody matches "${esc(state.query)}". Try the full SteamID64.` : "Nothing in this view."}
`; } return ` ${kind === "comm" ? "" : ""} ${rows.map((p) => ` ${kind === "comm" ? `` : ""} `).join("")}
PlayerTypeReason Length Issued Status
${esc(p.name || "Unknown")}${esc(p.steamid)}
${COMM_LABEL[p.type]}${esc(p.reason)} ${termCell(p)} ${fmtDate(p.created)} ${statusTag(p)}
`; } async function loadResults() { const box = $("#results"); if (!box) return; const bans = state.page === "bans"; const path = bans ? "/api/public/bans" : "/api/public/comms"; const status = bans ? state.banFilter : state.commFilter; const want = `${state.page}|${status}|${state.query}|${state.num}`; loadResults.want = want; try { const list = await api(path + qs({ status, q: state.query, page: state.num })); if (loadResults.want !== want) return; box.innerHTML = publicTable(list.items, bans ? "ban" : "comm") + pager(list); const lead = $("#lead"); if (lead && bans) lead.textContent = `${list.active} ${list.active === 1 ? "player is" : "players are"} banned right now. Search for a name or SteamID64 to check a ban and when it ends.`; } catch (err) { box.innerHTML = errorBox(err); } } function bans() { $("#view").innerHTML = `

Bans

Search for a name or SteamID64 to check a ban and when it ends.

${searchBox("Search by name, SteamID64 or reason", 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. Blocks carry over between maps and reconnects.

${searchBox("Search by name, SteamID64 or reason", state.query)} ${segmented("commFilter", state.commFilter, [["active", "Active"], ["GAG", "Gags"], ["MUTE", "Mutes"], ["SILENCE", "Silences"], ["expired", "Expired"], ["all", "All"]])}
${loading()}
`; loadResults(); } /* ---------- Staff ---------- */ async function staff() { const v = $("#view"); v.innerHTML = loading(); let data; try { data = await api("/api/public/staff"); } catch (err) { v.innerHTML = errorBox(err); return; } if (state.page !== "staff") return; const rankRow = (g) => `

${esc(g.label)}

${g.about ? `

${esc(g.about)}

` : ""}
`; const staffGroups = data.groups.filter((g) => !g.supporter); const supporters = data.groups.filter((g) => g.supporter); const online = data.staffOnline; v.innerHTML = `

Staff

${online ? `${online} staff ${online === 1 ? "is" : "are"} on the server right now.` : "No staff are on the server right now."}

${staffGroups.length ? `
${staffGroups.map(rankRow).join("")}
` : '
No staff are listed yet.
'} ${supporters.length ? `

Supporters

${supporters.map(rankRow).join("")}
` : ""}`; } /* ---------- Player lookup (read-only) ---------- */ async function openPlayer(sid) { $("#drawer-root").innerHTML = `
`; let p; try { p = await api(`/api/public/players/${encodeURIComponent(sid)}`); } catch (err) { $(".drawer").innerHTML = errorBox(err); return; } const history = [...p.bans, ...p.comms].sort((a, b) => new Date(b.created) - new Date(a.created)); const now = history.filter((h) => h.status === "active"); const label = (h) => (h.kind === "ban" ? "Ban" : COMM_LABEL[h.type]); const name = p.name || "Unknown player"; $("#drawer-root").innerHTML = `
`; $(".drawer .close").focus(); } /* ---------- Routing & events ---------- */ const RENDER = { server, bans, comms, staff }; function route() { const page = location.hash.slice(1); state.page = RENDER[page] ? page : "server"; $("#nav-links").innerHTML = PAGES.map((p) => `
  • ${p.label}
  • `).join(""); RENDER[state.page](); } window.addEventListener("hashchange", () => { state.query = ""; state.num = 1; route(); window.scrollTo(0, 0); }); const search = debounce(() => loadResults(), 250); document.addEventListener("input", (e) => { if (e.target.id === "q") { state.query = e.target.value; state.num = 1; search(); } }); document.addEventListener("click", (e) => { const t = e.target; const seg = t.closest("[data-seg] button"); if (seg) { state[seg.parentElement.dataset.seg] = seg.dataset.value; state.num = 1; seg.parentElement.querySelectorAll("button").forEach((b) => b.setAttribute("aria-pressed", String(b === seg))); loadResults(); return; } const pg = t.closest("[data-page]"); if (pg) { state.num = Number(pg.dataset.page); loadResults(); return; } if (t.closest("[data-close]")) { closeDrawer(); return; } const row = t.closest("[data-player]"); if (row) openPlayer(row.dataset.player); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("#drawer-root").innerHTML) closeDrawer(); }); api("/api/me").then((me) => { $("#site-name").innerHTML = siteName(me.site); document.title = `${me.site} bans and staff`; if (me.staff) $(".signin").innerHTML = "Staff panel"; }).catch(() => {}); // Keep the scoreboard current while it's on screen. setInterval(() => { if (state.page === "server" && !document.hidden && !$("#drawer-root").innerHTML) server(); }, 15000); route();