Busiest hours count players; fewer descriptions; no Verified badges

Busiest hours and the heatmap count the different players on T or CT in
each hour, open segments included, instead of averaging player-hours, so
one player for part of an hour is 1, not 0.38. Their descriptions end in
the span covered (Last 30 days, or Since <date>). Side-by-side chart panels
match heights, and Players per day notes its busiest day.

Dropped the page descriptions on Bans, Gags & mutes, Staff and Playtime,
and the Verified badges on the staff Overview scoreboard and drawer header;
the Verification page and the drawer's Verification section still show it.
The GroupVerify DB test now checks the staff drawer, since the public site
says nothing about verification.
This commit is contained in:
Astra 2026-10-04 22:49:12 +01:00
parent 2516759fa9
commit ba61708bed
11 changed files with 88 additions and 81 deletions

View file

@ -112,27 +112,31 @@ function ptLeaders(d) {
</section>`;
}
// "the last 30 days", or how long the recording's been going when that's shorter.
function ptDaysText(n) {
if (n >= 30) return "the last 30 days";
if (n >= 28) return "the last 4 weeks";
return n > 1 ? `the ${n} days since recording began` : "today, the first day recorded";
// The span an average covers: "Last 30 days." (or 4 weeks), or "Since 4 Oct." while the
// recording is younger than that. n is the number of days, today included.
function ptSpan(n, full) {
if (n >= full) return full === 28 ? "Last 4 weeks." : "Last 30 days.";
const from = new Date();
from.setDate(from.getDate() - Math.max(0, (n || 1) - 1));
return `Since ${from.toLocaleDateString(undefined, { day: "numeric", month: "short" })}.`;
}
function ptBusiest(o) {
const vals = o.byHour.map((v, h) => ({ v, tick: h % 3 === 0 ? ptPad(h) : "", tip: `${ptPad(h)}:00 · ${v} players on average` }));
const players = (v) => `${v} ${v === 1 ? "player" : "players"}`;
const vals = o.byHour.map((v, h) => ({ v, tick: h % 3 === 0 ? ptPad(h) : "", tip: `${ptPad(h)}:00 · ${players(v)} a day` }));
const best = o.byHour.indexOf(Math.max(...o.byHour));
return `<section class="panel">
<div class="panel-head"><div><h2>Busiest hours</h2><p>Players on T or CT (bots don't count), averaged over ${ptDaysText(o.byHourDays)}, in your time zone.</p></div></div>
${ptColumns(vals, { note: o.byHour[best] ? `Busiest at <b>${ptPad(best)}:00</b>, with ${o.byHour[best]} players on average.` : "" })}
<div class="panel-head"><div><h2>Busiest hours</h2><p>Players on T or CT in each hour of the day, on average. Bots don't count. ${ptSpan(o.byHourDays, 30)}</p></div></div>
${ptColumns(vals, { note: o.byHour[best] ? `Busiest at <b>${ptPad(best)}:00</b>, with ${players(o.byHour[best])} a day on average.` : "No one has played on a team yet." })}
</section>`;
}
function ptDaily(o) {
const vals = o.days.map((d, i) => ({ v: d.players || 0, tick: i % 5 === 0 ? ptDay(d.date, { day: "numeric" }) : "", tip: `${ptDay(d.date)} · ${d.players || 0} players` }));
const best = o.days.reduce((b, d) => ((d.players || 0) > (b?.players || 0) ? d : b), null);
return `<section class="panel">
<div class="panel-head"><div><h2>Players per day</h2><p>Different players who joined each day, last 30 days.</p></div></div>
${ptColumns(vals, { fill: "var(--ct)", whole: true })}
${ptColumns(vals, { fill: "var(--ct)", whole: true, note: best ? `Busiest day: <b>${ptDay(best.date)}</b>, with ${best.players} ${best.players === 1 ? "player" : "players"}.` : "No one has joined yet." })}
</section>`;
}
@ -140,7 +144,7 @@ function ptHeatmap(o) {
const max = Math.max(0.1, ...o.heat.flat());
const step = (v) => (v <= 0.05 ? 0 : Math.min(5, Math.ceil((v / max) * 5)));
return `<section class="panel">
<div class="panel-head"><div><h2>When people play</h2><p>Average players on T or CT by day and hour, over ${ptDaysText(o.heatDays)}, in your time zone.</p></div></div>
<div class="panel-head"><div><h2>When people play</h2><p>Players on T or CT by day and hour, on average. ${ptSpan(o.heatDays, 28)}</p></div></div>
<div class="pt-heat"><table>
<thead><tr><th></th>${o.heat[0].map((_, h) => `<th>${h % 3 === 0 ? ptPad(h) : ""}</th>`).join("")}</tr></thead>
<tbody>${o.heat.map((row, wd) => `<tr><th scope="row">${PT_WEEKDAYS[wd]}</th>${row.map((v, h) => `<td><i style="background:var(--heat-${step(v)})" data-tip="${PT_WEEKDAYS[wd]} ${ptPad(h)}:00 · ${v} players"></i></td>`).join("")}</tr>`).join("")}</tbody>
@ -268,9 +272,7 @@ async function ptPage(me, isCurrent) {
const v = $("#view");
const staff = !!me?.playtimeStaff;
v.innerHTML = `
<div class="page-head"><div><h1>Playtime</h1><p class="sub">${staff
? "Who's on, staff activity and where players come from, then everything players see on the public site."
: "Who plays the most, and when the server is busiest. Click a player for their stats."}</p></div></div>
<div class="page-head"><div><h1>Playtime</h1></div></div>
${staff ? `<div id="pt-staff">${loading()}</div><h2 class="crew-sep">${me.playtime === "on" ? "On the public site" : ""}</h2>` : ""}
<div id="pt-public" class="stack pt-stack">${me?.playtime === "on" ? loading() : ""}</div>`;
if (me?.playtime !== "on") {