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:
parent
2516759fa9
commit
ba61708bed
11 changed files with 88 additions and 81 deletions
|
|
@ -125,9 +125,9 @@ verified players perks. It keeps everything in SimpleAdmin's database (`gv_verif
|
|||
it to the ranks that should have it in the rank editor), staff give overrides for a day up to a
|
||||
year or with no expiry, change them and remove them. Root sets the group and turns verification
|
||||
on and off.
|
||||
- **The public site** shows nothing about verification. On the staff panel, the Overview
|
||||
scoreboard shows a "Verified" tag only while verification is on (enabled, with a group set) and
|
||||
GroupVerify isn't missing from the server's plugin list.
|
||||
- **No Verified tags** anywhere else: not on the public site, the scoreboards or player headers.
|
||||
Who's verified is on the Verification page, and why in each player drawer's Verification
|
||||
section.
|
||||
- **Clan tag**: Root can set the scoreboard tag verified players without a rank get (GroupVerify's
|
||||
`clan_tag`, at most 12 characters). CS2-Tags (1.0.4c-zio6+) applies it, and gives unverified
|
||||
players without a rank no tag at all. Blank turns it off.
|
||||
|
|
|
|||
|
|
@ -150,6 +150,36 @@ func (s *Store) hours(ctx context.Context, since time.Time, steamid uint64) ([]B
|
|||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// present lists who was on T or CT in each UTC hour since a time, once per player per hour,
|
||||
// including the segments still open (up to now).
|
||||
func (s *Store) present(ctx context.Context, since time.Time) ([]Bucket, []uint64, error) {
|
||||
where, args := s.and("server_id", []any{since, since, since.Add(-24 * time.Hour)})
|
||||
rows, err := s.db.QueryContext(ctx, `WITH RECURSIVE seg AS (
|
||||
SELECT steamid64, GREATEST(started_at, ?) AS t0, COALESCE(ended_at, UTC_TIMESTAMP()) AS t1 FROM pt_segments
|
||||
WHERE team IN (2, 3) AND COALESCE(ended_at, UTC_TIMESTAMP()) > ? AND started_at >= ?`+where+`
|
||||
), h AS (
|
||||
SELECT steamid64, t1, CAST(DATE_FORMAT(t0, '%Y-%m-%d %H:00:00') AS DATETIME) AS b FROM seg WHERE t1 > t0
|
||||
UNION ALL
|
||||
SELECT steamid64, t1, b + INTERVAL 1 HOUR FROM h WHERE b + INTERVAL 1 HOUR < t1
|
||||
)
|
||||
SELECT DISTINCT b, steamid64 FROM h`, args...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var hours []Bucket
|
||||
var ids []uint64
|
||||
for rows.Next() {
|
||||
var b Bucket
|
||||
var id uint64
|
||||
if err := rows.Scan(&b.Start, &id); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hours, ids = append(hours, b), append(ids, id)
|
||||
}
|
||||
return hours, ids, rows.Err()
|
||||
}
|
||||
|
||||
// dayStart is local midnight on t's day in loc.
|
||||
func dayStart(t time.Time, loc *time.Location) time.Time {
|
||||
y, m, d := t.In(loc).Date()
|
||||
|
|
@ -230,9 +260,11 @@ type MapTime struct {
|
|||
// Overview is the public Playtime page. Week figures are the last 7 days; charts the last 30
|
||||
// (the heatmap the last 28, four of each weekday), in the viewer's time zone.
|
||||
type Overview struct {
|
||||
Summary Summary `json:"summary"`
|
||||
ByHour [24]float64 `json:"byHour"` // average players on T/CT in each local hour of the day
|
||||
Heat [7][24]float64 `json:"heat"` // the same per weekday, Monday first
|
||||
Summary Summary `json:"summary"`
|
||||
// How many different players were on T or CT in each local hour of the day, on average per
|
||||
// day; Heat is the same per weekday, Monday first.
|
||||
ByHour [24]float64 `json:"byHour"`
|
||||
Heat [7][24]float64 `json:"heat"`
|
||||
// The days ByHour and Heat average over: the last 30 (28 for Heat), or fewer while the
|
||||
// recording is younger than that, so a new server doesn't look emptier than it is.
|
||||
ByHourDays int `json:"byHourDays"`
|
||||
|
|
@ -276,13 +308,20 @@ func (s *Store) Overview(ctx context.Context, now time.Time, loc *time.Location)
|
|||
o.ByHourDays++
|
||||
}
|
||||
|
||||
// Each player counts once for every hour they were on T or CT in.
|
||||
present, _, err := s.present(ctx, start)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
for _, b := range present {
|
||||
local := b.Start.In(loc)
|
||||
o.ByHour[local.Hour()] += 1 / float64(max(o.ByHourDays, 1))
|
||||
if wd := (int(local.Weekday()) + 6) % 7; !local.Before(heatFrom) && weekdays[wd] > 0 {
|
||||
o.Heat[wd][local.Hour()] += 1 / float64(weekdays[wd])
|
||||
}
|
||||
}
|
||||
for _, b := range buckets {
|
||||
h := float64(b.Seconds) / 3600
|
||||
local := b.Start.In(loc)
|
||||
o.ByHour[local.Hour()] += h / float64(max(o.ByHourDays, 1))
|
||||
if wd := (int(local.Weekday()) + 6) % 7; !local.Before(heatFrom) && weekdays[wd] > 0 {
|
||||
o.Heat[wd][local.Hour()] += h / float64(weekdays[wd])
|
||||
}
|
||||
switch {
|
||||
case !b.Start.Before(week):
|
||||
o.Summary.Hours += h
|
||||
|
|
@ -290,7 +329,7 @@ func (s *Store) Overview(ctx context.Context, now time.Time, loc *time.Location)
|
|||
o.Summary.HoursPrev += h
|
||||
}
|
||||
}
|
||||
// Two decimals: a quiet hour averages well under one player.
|
||||
// Two decimals: averaged over days, a quiet hour is a fraction of a player.
|
||||
for i := range o.ByHour {
|
||||
o.ByHour[i] = round2(o.ByHour[i])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,12 +172,14 @@ func TestPlaytimeDB(t *testing.T) {
|
|||
for _, h := range o.ByHour {
|
||||
byHour += h
|
||||
}
|
||||
// Recording began with Bravo's session 10 days ago, so 3.5 h spread over those 11 days.
|
||||
// Recording began with Bravo's session 10 days ago, so over those 11 days. Each player counts
|
||||
// once per hour they were on T or CT in: Alpha in 2 hours, Bravo in 3 ten days ago and in 2 or
|
||||
// 3 now (the open segment runs to the database's clock).
|
||||
if o.ByHourDays != 11 || o.HeatDays != 11 {
|
||||
t.Fatalf("averaged over %d days (heatmap %d), want 11", o.ByHourDays, o.HeatDays)
|
||||
}
|
||||
if want := 3.5 / 11; byHour < want-0.05 || byHour > want+0.05 {
|
||||
t.Fatalf("byHour adds up to %.3f, want about %.3f", byHour, want)
|
||||
if n := byHour * 11; n < 6.8 || n > 8.2 {
|
||||
t.Fatalf("%.2f player-hours over the days, want 7 or 8", n)
|
||||
}
|
||||
// Alpha on one day, Bravo on two days ten days apart.
|
||||
players := 0
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ func nonNil[T any](v []T) []T {
|
|||
func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request, id *Identity, x *staffIndex) {
|
||||
ctx := r.Context()
|
||||
view := s.serverView(x, s.poll.Current())
|
||||
s.markVerified(ctx, view.Players)
|
||||
|
||||
var ids []string
|
||||
for _, p := range view.Players {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function onlineTable(players) {
|
|||
<thead><tr><th>Player</th><th class="hide-sm">Rank</th><th class="num">K / D</th><th class="num hide-sm">Ping</th><th><span class="sr">Actions</span></th></tr></thead>
|
||||
<tbody>
|
||||
${list.map((p) => `<tr class="clickable" data-player="${esc(p.steamid)}">
|
||||
<td><div class="who"><span class="team ${p.team === 3 ? "ct" : p.team === 2 ? "t" : ""}">${teamName[p.team] || "SPEC"}</span><div><b>${esc(p.name)}</b> ${p.verified ? VERIFIED_TAG : ""} ${tagsFor(p)}</div></div></td>
|
||||
<td><div class="who"><span class="team ${p.team === 3 ? "ct" : p.team === 2 ? "t" : ""}">${teamName[p.team] || "SPEC"}</span><div><b>${esc(p.name)}</b> ${tagsFor(p)}</div></div></td>
|
||||
<td class="hide-sm">${rankTag(p.rank)}</td>
|
||||
<td class="num">${p.kills} / ${p.deaths}</td>
|
||||
<td class="num hide-sm ${p.ping > 80 ? "" : "muted"}">${p.ping}</td>
|
||||
|
|
@ -773,7 +773,7 @@ async function openPlayer(sid) {
|
|||
<aside class="drawer" role="dialog" aria-modal="true" aria-label="${esc(name)}">
|
||||
<div class="drawer-head">
|
||||
<span class="avatar lg">${initial(name)}</span>
|
||||
<div><h2>${esc(name)}</h2><div>${rankTag(d.rank)} ${d.online ? '<span class="tag green" style="margin-left:6px">Online</span>' : ""}${d.verify?.verified ? ` ${VERIFIED_TAG}` : ""}</div></div>
|
||||
<div><h2>${esc(name)}</h2><div>${rankTag(d.rank)} ${d.online ? '<span class="tag green" style="margin-left:6px">Online</span>' : ""}</div></div>
|
||||
<button class="close" type="button" data-close aria-label="Close">×</button>
|
||||
</div>
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -115,8 +115,6 @@ async function loadResults() {
|
|||
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);
|
||||
}
|
||||
|
|
@ -125,7 +123,7 @@ async function loadResults() {
|
|||
function bans() {
|
||||
$("#view").innerHTML = `
|
||||
<div class="page-head">
|
||||
<div><h1>Bans</h1><p class="sub" id="lead">Search for a name or SteamID64 to check a ban and when it ends.</p></div>
|
||||
<div><h1>Bans</h1></div>
|
||||
</div>
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
|
|
@ -140,7 +138,7 @@ function bans() {
|
|||
function comms() {
|
||||
$("#view").innerHTML = `
|
||||
<div class="page-head">
|
||||
<div><h1>Gags & mutes</h1><p class="sub">A gag blocks text chat, a mute blocks voice, and a silence blocks both. Blocks carry over between maps and reconnects.</p></div>
|
||||
<div><h1>Gags & mutes</h1></div>
|
||||
</div>
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
|
|
@ -173,10 +171,9 @@ async function staff() {
|
|||
</div>`;
|
||||
const staffGroups = data.groups.filter((g) => !g.supporter);
|
||||
const supporters = data.groups.filter((g) => g.supporter);
|
||||
const online = data.staffOnline;
|
||||
v.innerHTML = `
|
||||
<div class="page-head">
|
||||
<div><h1>Staff</h1><p class="sub">${online ? `${online} staff ${online === 1 ? "is" : "are"} on the server right now.` : "No staff are on the server right now."}</p></div>
|
||||
<div><h1>Staff</h1></div>
|
||||
</div>
|
||||
${staffGroups.length ? `<section class="panel crew-panel">${staffGroups.map(rankRow).join("")}</section>` : '<div class="empty">No staff are listed yet.</div>'}
|
||||
${supporters.length ? `<h2 class="crew-sep">Supporters</h2><section class="panel crew-panel">${supporters.map(rankRow).join("")}</section>` : ""}
|
||||
|
|
|
|||
|
|
@ -1199,7 +1199,11 @@ tr:nth-child(3) .pt-place { color: #d08a52; }
|
|||
.drawer .pt-credit { border-top: 0; padding: 14px 22px; }
|
||||
.pt-lines { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; font-size: 14px; }
|
||||
.pt-stack > *, .pt-grid > * { min-width: 0; }
|
||||
.pt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: start; }
|
||||
/* Side-by-side panels match heights; charts sit at the top and notes at the bottom. */
|
||||
.pt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: stretch; }
|
||||
.pt-grid > .panel { display: flex; flex-direction: column; }
|
||||
.pt-grid > .panel > .pt-cols { flex: 1; display: flex; flex-direction: column; }
|
||||
.pt-grid > .panel > .pt-cols .note { margin-top: auto; padding-top: 10px; }
|
||||
|
||||
#pt-tip {
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ func toPublic(ps []store.Penalty) []publicPenalty {
|
|||
type livePlayer struct {
|
||||
live.Player
|
||||
Rank *rankView `json:"rank,omitempty"`
|
||||
// Verified by GroupVerify, when that's shown at all (see verifyInfo.Shown). Staff overview only.
|
||||
Verified bool `json:"verified,omitempty"`
|
||||
// Staff-only fields, left empty on the public page.
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
CanTarget bool `json:"canTarget,omitempty"`
|
||||
|
|
|
|||
|
|
@ -87,38 +87,6 @@ func (s *Server) verifyInfo(ctx context.Context) (verifyInfo, error) {
|
|||
return verifyInfo{Tables: tables, Settings: settings, Plugin: state, Version: version}, nil
|
||||
}
|
||||
|
||||
// verifiedSet returns which of these players are verified, or nil when verification isn't shown.
|
||||
// Errors are logged and treated as not shown: verification is extra, never worth failing a page.
|
||||
func (s *Server) verifiedSet(ctx context.Context, steamids []string) map[string]store.VerifyStatus {
|
||||
info, err := s.verifyInfo(ctx)
|
||||
if err == nil && info.Shown() {
|
||||
var st map[string]store.VerifyStatus
|
||||
if st, err = s.st.VerifyStatuses(ctx, info.Settings.Location, steamids); err == nil {
|
||||
return st
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("GroupVerify status unavailable", "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markVerified sets Verified on the live players who are.
|
||||
func (s *Server) markVerified(ctx context.Context, players []livePlayer) {
|
||||
var ids []string
|
||||
for _, p := range players {
|
||||
if !p.Bot {
|
||||
ids = append(ids, p.SteamID)
|
||||
}
|
||||
}
|
||||
set := s.verifiedSet(ctx, ids)
|
||||
for i, p := range players {
|
||||
if st, ok := set[p.SteamID]; ok && st.Verified() {
|
||||
players[i].Verified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Staff API
|
||||
|
||||
type verifyStatusView struct {
|
||||
|
|
|
|||
|
|
@ -176,8 +176,9 @@ func TestVerifyDB(t *testing.T) {
|
|||
if got := verified("/api/admin/players/"+sid, mod); got != want {
|
||||
t.Errorf("staff view of %s: verified %v, want %v", sid, got, want)
|
||||
}
|
||||
if got := verified("/api/public/players/"+sid, ""); got != want {
|
||||
t.Errorf("public view of %s: verified %v, want %v", sid, got, want)
|
||||
// The public site never says whether anyone is verified.
|
||||
if _, out := call("GET", "/api/public/players/"+sid, "", nil); out["verified"] != nil {
|
||||
t.Errorf("public view of %s says verified %v", sid, out["verified"])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,17 +187,17 @@ func TestVerifyDB(t *testing.T) {
|
|||
expect(200, code, out, "remove override")
|
||||
code, out = call("DELETE", "/api/admin/verify/overrides/"+charlie, mod, nil)
|
||||
expect(404, code, out, "remove it again")
|
||||
if verified("/api/public/players/"+charlie, "") {
|
||||
if verified("/api/admin/players/"+charlie, mod) {
|
||||
t.Error("still verified after removing the override")
|
||||
}
|
||||
|
||||
// Turned off, nobody is verified, publicly or for staff.
|
||||
// Turned off, nobody is verified.
|
||||
code, out = call("POST", "/api/admin/verify/settings", owner, map[string]any{"enabled": false})
|
||||
expect(200, code, out, "turn off")
|
||||
if !strings.Contains(out["message"].(string), "off") {
|
||||
t.Errorf("turn-off message %q", out["message"])
|
||||
}
|
||||
if verified("/api/public/players/"+member, "") || verified("/api/admin/players/"+member, mod) {
|
||||
if verified("/api/admin/players/"+member, mod) {
|
||||
t.Error("verified while GroupVerify is off")
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +215,7 @@ func TestVerifyDB(t *testing.T) {
|
|||
if want := time.Now().Add(24 * time.Hour).In(tokyo).Format("2006-01-02 15:04"); raw != want && raw != time.Now().Add(24*time.Hour+time.Minute).In(tokyo).Format("2006-01-02 15:04") {
|
||||
t.Errorf("expires_at %s, want Tokyo wall time %s", raw, want)
|
||||
}
|
||||
if !verified("/api/public/players/"+charlie, "") {
|
||||
if !verified("/api/admin/players/"+charlie, mod) {
|
||||
t.Error("override in Tokyo time isn't counted")
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +231,4 @@ func TestVerifyDB(t *testing.T) {
|
|||
if _, out := call("GET", "/api/admin/players/"+charlie, mod, nil); out["verify"] != nil {
|
||||
t.Errorf("player drawer has verify %v without the tables", out["verify"])
|
||||
}
|
||||
if verified("/api/public/players/"+charlie, "") {
|
||||
t.Error("publicly verified without the tables")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue