Playtime from PlaytimeHistory's database (SAW_PT_DSN)

A public Playtime tab and player-drawer playtime, and staff-only figures
(who's on, staff activity, disconnect reasons, countries, names, sessions)
for @css/generic on the staff panel and on the public site when signed in.
Hidden unless SAW_PT_DSN is set and its tables are there.
This commit is contained in:
Astra 2026-10-04 21:05:57 +01:00
parent daa7ec2dbc
commit 6d0fc6e463
18 changed files with 2238 additions and 12 deletions

View file

@ -1,14 +1,18 @@
// Public, read-only site: the live server, bans, gags/mutes, and staff with their rank changes. The
// API leaves out who issued penalties and IPs; nothing here can change anything.
// Public, read-only site: the live server, bans, gags/mutes, staff with their rank changes, and
// playtime (playtime.js). The API leaves out who issued penalties and IPs; nothing here can change
// anything. Signed-in staff with @css/generic also see staff playtime figures here.
const PAGES = [
{ id: "server", label: "Server" },
{ id: "bans", label: "Bans" },
{ id: "comms", label: "Gags & mutes" },
{ id: "staff", label: "Staff" },
// Once PlaytimeHistory has recorded something, or for staff who see its staff figures.
{ id: "playtime", label: "Playtime", show: () => state.me?.playtime === "on" || !!state.me?.playtimeStaff },
];
const state = { page: "server", banFilter: "active", commFilter: "active", query: "", num: 1 };
// me is null until /api/me answers.
const state = { page: "server", banFilter: "active", commFilter: "active", query: "", num: 1, me: null };
function siteName(title) {
const i = title.indexOf(".");
@ -242,6 +246,7 @@ async function openPlayer(sid) {
${now.length || p.online || p.verified ? `<section>
<div class="chips">${p.verified ? VERIFIED_TAG : ""}${p.online ? '<span class="tag">On the server</span>' : ""}${now.map((h) => `<span class="tag ${h.kind === "ban" ? "red" : "amber"}">${h.kind === "ban" ? "Banned" : COMM_HELP[h.type]}${h.duration ? `, ${fmtLeft(served(h).left)}` : ", permanently"}</span>`).join("")}</div>
</section>` : ""}
${ptDrawer(p.playtime, p.online)}
<section>
<h3>History</h3>
${history.length ? `<ul class="history">${history.map((h) => `<li>
@ -256,11 +261,20 @@ async function openPlayer(sid) {
/* ---------- Routing & events ---------- */
const RENDER = { server, bans, comms, staff };
function playtime() {
if (!state.me) { $("#view").innerHTML = loading(); return; }
ptPage(state.me, () => state.page === "playtime");
}
const RENDER = { server, bans, comms, staff, playtime };
// A page is shown if it has no condition, or (once /api/me has answered) meets it.
const pageShown = (p) => !p.show || p.show();
function route() {
const [page, query] = location.hash.slice(1).split("?");
state.page = RENDER[page] ? page : "server";
const def = PAGES.find((p) => p.id === page);
state.page = def && (pageShown(def) || !state.me) ? page : "server";
// A link like #bans?q=<steamid> (from a staff notification) opens the list searching for that
// player, showing every status so lifted and expired ones are there too.
const q = new URLSearchParams(query || "").get("q");
@ -269,7 +283,7 @@ function route() {
state.num = 1;
state[state.page === "bans" ? "banFilter" : "commFilter"] = "all";
}
$("#nav-links").innerHTML = PAGES.map((p) => `<li><a href="#${p.id}" ${p.id === state.page ? 'aria-current="page"' : ""}>${p.label}</a></li>`).join("");
$("#nav-links").innerHTML = PAGES.filter(pageShown).map((p) => `<li><a href="#${p.id}" ${p.id === state.page ? 'aria-current="page"' : ""}>${p.label}</a></li>`).join("");
RENDER[state.page]();
}
@ -283,6 +297,7 @@ document.addEventListener("input", (e) => {
document.addEventListener("click", (e) => {
const t = e.target;
const seg = t.closest("[data-seg] button");
if (seg && seg.parentElement.dataset.seg.startsWith("pt")) return; // playtime.js
if (seg) {
state[seg.parentElement.dataset.seg] = seg.dataset.value;
state.num = 1;
@ -303,10 +318,15 @@ document.addEventListener("keydown", (e) => {
});
api("/api/me").then((me) => {
state.me = me;
$("#site-name").innerHTML = siteName(me.site);
document.title = `${me.site} bans and staff`;
if (me.staff) $(".signin").innerHTML = "Staff panel";
}).catch(() => {});
}).catch(() => { state.me = {}; }).finally(() => {
// The Playtime tab depends on /api/me, so draw the nav (and a Playtime page) again.
if (state.page === "playtime") route();
else $("#nav-links").innerHTML = PAGES.filter(pageShown).map((p) => `<li><a href="#${p.id}" ${p.id === state.page ? 'aria-current="page"' : ""}>${p.label}</a></li>`).join("");
});
// Keep the scoreboard current while it's on screen.
setInterval(() => { if (state.page === "server" && !document.hidden && !$("#drawer-root").innerHTML) server(); }, 15000);