Playtime stats between players only; no ping; page-wide grid
K/D, headshot %, kills and deaths (drawer, Top players, staff session lists) now come from pt_events, so kills of bots and deaths to bots don't count, as in Records; damage per round and MVPs are gone, since only the game's counters have them and those include bots. Average ping and the record count footnote are no longer shown. The server header's grid now runs behind the whole page.
This commit is contained in:
parent
c7fbff0e43
commit
2516759fa9
6 changed files with 120 additions and 81 deletions
|
|
@ -431,31 +431,63 @@ func (s *Store) Leaders(ctx context.Context, since *time.Time, limit int) ([]Lea
|
|||
return out, err
|
||||
}
|
||||
|
||||
in, args := inList(ids)
|
||||
q := `SELECT steamid64, COALESCE(SUM(kills), 0), COALESCE(SUM(deaths), 0) FROM pt_sessions WHERE steamid64 IN (` + in + `)`
|
||||
if since != nil {
|
||||
q += " AND started_at >= ?"
|
||||
args = append(args, *since)
|
||||
if !s.events.Load() {
|
||||
return out, nil
|
||||
}
|
||||
where, args := s.and("server_id", args)
|
||||
krows, err := s.db.QueryContext(ctx, q+where+" GROUP BY steamid64", args...)
|
||||
kd, err := s.killsDeaths(ctx, ids, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer krows.Close()
|
||||
kd := map[string][2]int{}
|
||||
for krows.Next() {
|
||||
for i, id := range ids {
|
||||
out[i].Kills, out[i].Deaths = kd[id].Kills, kd[id].Deaths
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// killsDeaths counts kills and deaths between players from pt_events, since a time or (nil)
|
||||
// ever. Kills of bots and deaths to bots, the world or themselves don't count: only a player
|
||||
// killing another player is logged with a victim.
|
||||
func (s *Store) killsDeaths(ctx context.Context, ids []uint64, since *time.Time) (map[uint64]Stats, error) {
|
||||
in, ids1 := inList(ids)
|
||||
_, ids2 := inList(ids)
|
||||
when := ""
|
||||
var args []any
|
||||
part := func(idArgs []any) []any {
|
||||
a := append([]any{}, idArgs...)
|
||||
if since != nil {
|
||||
a = append(a, *since)
|
||||
}
|
||||
if s.server != 0 {
|
||||
a = append(a, s.server)
|
||||
}
|
||||
return a
|
||||
}
|
||||
if since != nil {
|
||||
when = " AND at >= ?"
|
||||
}
|
||||
srv, _ := s.and("server_id", nil)
|
||||
args = append(part(ids1), part(ids2)...)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, SUM(k), SUM(d), SUM(h) FROM (
|
||||
SELECT steamid64 AS id, 1 AS k, 0 AS d, IF(headshot, 1, 0) AS h FROM pt_events
|
||||
WHERE kind = 1 AND NOT victim_bot AND steamid64 IN (`+in+`)`+when+srv+`
|
||||
UNION ALL
|
||||
SELECT victim_steamid64, 0, 1, 0 FROM pt_events
|
||||
WHERE kind = 1 AND victim_steamid64 IN (`+in+`)`+when+srv+`
|
||||
) x GROUP BY id`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[uint64]Stats{}
|
||||
for rows.Next() {
|
||||
var id uint64
|
||||
var k, d int
|
||||
if err := krows.Scan(&id, &k, &d); err != nil {
|
||||
var st Stats
|
||||
if err := rows.Scan(&id, &st.Kills, &st.Deaths, &st.Headshots); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kd[strconv.FormatUint(id, 10)] = [2]int{k, d}
|
||||
out[id] = st
|
||||
}
|
||||
for i := range out {
|
||||
out[i].Kills, out[i].Deaths = kd[out[i].SteamID][0], kd[out[i].SteamID][1]
|
||||
}
|
||||
return out, krows.Err()
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Record is one player's count in one of the Records categories.
|
||||
|
|
@ -566,14 +598,11 @@ type Events struct {
|
|||
Headshots int `json:"headshots"`
|
||||
}
|
||||
|
||||
// Stats is kills, deaths and headshot kills between players (pt_events, so 0.2.0 onward).
|
||||
type Stats struct {
|
||||
Kills int `json:"kills"`
|
||||
Deaths int `json:"deaths"`
|
||||
Assists int `json:"assists"`
|
||||
Headshots int `json:"headshots"`
|
||||
Damage int `json:"damage"`
|
||||
MVPs int `json:"mvps"`
|
||||
Rounds int `json:"rounds"`
|
||||
}
|
||||
|
||||
// Player is what anyone can see about one player's playtime. Hours are on T or CT unless named.
|
||||
|
|
@ -586,7 +615,7 @@ type Player struct {
|
|||
Spec float64 `json:"spec"` // spectating or unassigned
|
||||
Alive float64 `json:"alive"`
|
||||
Dead float64 `json:"dead"`
|
||||
Stats Stats `json:"stats"`
|
||||
Stats *Stats `json:"stats,omitempty"` // nil without pt_events
|
||||
Events *Events `json:"events,omitempty"` // nil without pt_events
|
||||
Days []Day `json:"days"` // last 30 days
|
||||
Maps []MapTime `json:"maps"`
|
||||
|
|
@ -607,11 +636,13 @@ func (s *Store) Player(ctx context.Context, steamid uint64, now time.Time, loc *
|
|||
h := func(secs int64) float64 { return round1(float64(secs) / 3600) }
|
||||
p.T, p.CT, p.Spec, p.Alive, p.Dead = h(t), h(ct), h(spec), h(alive), h(dead)
|
||||
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(kills), 0), COALESCE(SUM(deaths), 0), COALESCE(SUM(assists), 0),
|
||||
COALESCE(SUM(headshots), 0), COALESCE(SUM(damage), 0), COALESCE(SUM(mvp), 0), COALESCE(SUM(rounds_played), 0)
|
||||
FROM pt_sessions WHERE steamid64 = ?`, steamid).
|
||||
Scan(&p.Stats.Kills, &p.Stats.Deaths, &p.Stats.Assists, &p.Stats.Headshots, &p.Stats.Damage, &p.Stats.MVPs, &p.Stats.Rounds); err != nil {
|
||||
return nil, err
|
||||
if s.events.Load() {
|
||||
kd, err := s.killsDeaths(ctx, []uint64{steamid}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st := kd[steamid]
|
||||
p.Stats = &st
|
||||
}
|
||||
|
||||
if s.events.Load() {
|
||||
|
|
@ -655,22 +686,20 @@ type Session struct {
|
|||
Reason string `json:"reason,omitempty"`
|
||||
Kills *int `json:"kills,omitempty"`
|
||||
Deaths *int `json:"deaths,omitempty"`
|
||||
Ping *int `json:"ping,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
}
|
||||
|
||||
// PlayerStaff is what only staff see: names, sessions, location and ping.
|
||||
// PlayerStaff is what only staff see: names, sessions and location.
|
||||
type PlayerStaff struct {
|
||||
Names []Nickname `json:"names"`
|
||||
Recent []Session `json:"recent"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryName string `json:"countryName,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Ping *int `json:"ping,omitempty"` // average over the last 30 days
|
||||
}
|
||||
|
||||
func (s *Store) PlayerStaff(ctx context.Context, steamid uint64, now time.Time) (PlayerStaff, error) {
|
||||
func (s *Store) PlayerStaff(ctx context.Context, steamid uint64) (PlayerStaff, error) {
|
||||
out := PlayerStaff{Names: []Nickname{}, Recent: []Session{}}
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT nickname, first_seen, last_seen, times_seen FROM pt_nicknames
|
||||
WHERE steamid64 = ? ORDER BY last_seen DESC LIMIT 20`, steamid)
|
||||
|
|
@ -690,19 +719,28 @@ func (s *Store) PlayerStaff(ctx context.Context, steamid uint64, now time.Time)
|
|||
return out, err
|
||||
}
|
||||
|
||||
where, args := s.and("server_id", []any{steamid})
|
||||
rows, err = s.db.QueryContext(ctx, `SELECT started_at, ended_at, COALESCE(duration_seconds, TIMESTAMPDIFF(SECOND, started_at, heartbeat_at)),
|
||||
end_kind, connect_map, COALESCE(disconnect_map, ''), COALESCE(disconnect_reason_name, ''), kills, deaths, ping_avg,
|
||||
COALESCE(country_iso, ''), COALESCE(city, '')
|
||||
FROM pt_sessions WHERE steamid64 = ?`+where+` ORDER BY started_at DESC LIMIT 10`, args...)
|
||||
// Kills and deaths between players from pt_events, for sessions since it began; NULL before.
|
||||
kd := "NULL, NULL"
|
||||
if s.events.Load() {
|
||||
kd = `IF(s.started_at >= (SELECT MIN(at) FROM pt_events),
|
||||
(SELECT COUNT(*) FROM pt_events e WHERE e.session_key = s.session_key AND e.kind = 1 AND NOT e.victim_bot), NULL),
|
||||
IF(s.started_at >= (SELECT MIN(at) FROM pt_events),
|
||||
(SELECT COUNT(*) FROM pt_events e WHERE e.kind = 1 AND e.victim_steamid64 = s.steamid64
|
||||
AND e.at >= s.started_at AND e.at <= COALESCE(s.ended_at, s.heartbeat_at, UTC_TIMESTAMP())), NULL)`
|
||||
}
|
||||
where, args := s.and("s.server_id", []any{steamid})
|
||||
rows, err = s.db.QueryContext(ctx, `SELECT s.started_at, s.ended_at, COALESCE(s.duration_seconds, TIMESTAMPDIFF(SECOND, s.started_at, s.heartbeat_at)),
|
||||
s.end_kind, s.connect_map, COALESCE(s.disconnect_map, ''), COALESCE(s.disconnect_reason_name, ''), `+kd+`,
|
||||
COALESCE(s.country_iso, ''), COALESCE(s.city, '')
|
||||
FROM pt_sessions s WHERE s.steamid64 = ?`+where+` ORDER BY s.started_at DESC LIMIT 10`, args...)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var x Session
|
||||
var ended sql.NullTime
|
||||
var kills, deaths, ping sql.NullInt64
|
||||
if err := rows.Scan(&x.Started, &ended, &x.Seconds, &x.EndKind, &x.Map, &x.EndMap, &x.Reason, &kills, &deaths, &ping,
|
||||
var kills, deaths sql.NullInt64
|
||||
if err := rows.Scan(&x.Started, &ended, &x.Seconds, &x.EndKind, &x.Map, &x.EndMap, &x.Reason, &kills, &deaths,
|
||||
&x.Country, &x.City); err != nil {
|
||||
rows.Close()
|
||||
return out, err
|
||||
|
|
@ -710,7 +748,7 @@ func (s *Store) PlayerStaff(ctx context.Context, steamid uint64, now time.Time)
|
|||
if ended.Valid {
|
||||
x.Ended = &ended.Time
|
||||
}
|
||||
x.Kills, x.Deaths, x.Ping = intPtr(kills), intPtr(deaths), intPtr(ping)
|
||||
x.Kills, x.Deaths = intPtr(kills), intPtr(deaths)
|
||||
out.Recent = append(out.Recent, x)
|
||||
}
|
||||
rows.Close()
|
||||
|
|
@ -724,15 +762,6 @@ func (s *Store) PlayerStaff(ctx context.Context, steamid uint64, now time.Time)
|
|||
if err != nil && err != sql.ErrNoRows {
|
||||
return out, err
|
||||
}
|
||||
var ping sql.NullFloat64
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT AVG(ping_avg) FROM pt_sessions WHERE steamid64 = ? AND started_at >= ?`,
|
||||
steamid, now.Add(-30*24*time.Hour)).Scan(&ping); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if ping.Valid {
|
||||
n := int(ping.Float64 + 0.5)
|
||||
out.Ping = &n
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,18 +116,20 @@ func TestPlaytimeDB(t *testing.T) {
|
|||
|
||||
// Events: Alpha plants twice (once 10 days ago), defuses once and knifes a player and a bot;
|
||||
// Bravo gets a Zeus kill and a headshot.
|
||||
event := func(id uint64, kind int, at time.Time, weapon any, hs any, bot any) {
|
||||
// Kills say who died (nil for a bot) and the killer's session.
|
||||
event := func(id uint64, sess string, kind int, at time.Time, weapon any, hs any, victim any, bot any) {
|
||||
t.Helper()
|
||||
exec(`INSERT INTO pt_events (event_key, server_id, steamid64, kind, at, map, team, weapon, headshot, victim_bot)
|
||||
VALUES (REPLACE(UUID(), '-', ''), 1, ?, ?, ?, 'de_mirage', 2, ?, ?, ?)`, id, kind, at, weapon, hs, bot)
|
||||
exec(`INSERT INTO pt_events (event_key, server_id, session_key, steamid64, kind, at, map, team, weapon, headshot,
|
||||
victim_steamid64, victim_bot)
|
||||
VALUES (REPLACE(UUID(), '-', ''), 1, NULLIF(?, ''), ?, ?, ?, 'de_mirage', 2, ?, ?, ?, ?)`, sess, id, kind, at, weapon, hs, victim, bot)
|
||||
}
|
||||
event(a, EventPlanted, ago(2*time.Hour), nil, nil, nil)
|
||||
event(a, EventPlanted, ago(10*24*time.Hour), nil, nil, nil)
|
||||
event(a, EventDefused, ago(2*time.Hour), nil, nil, nil)
|
||||
event(a, EventKill, ago(2*time.Hour), "knife_karambit", 0, 0)
|
||||
event(a, EventKill, ago(2*time.Hour), "knife", 1, 1) // a bot: doesn't count, not even as a headshot
|
||||
event(b, EventKill, ago(20*time.Minute), "taser", 0, 0)
|
||||
event(b, EventKill, ago(20*time.Minute), "ak47", 1, 0)
|
||||
event(a, "", EventPlanted, ago(10*24*time.Hour), nil, nil, nil, nil)
|
||||
event(a, "a1", EventPlanted, ago(2*time.Hour), nil, nil, nil, nil)
|
||||
event(a, "a1", EventDefused, ago(2*time.Hour), nil, nil, nil, nil)
|
||||
event(a, "a1", EventKill, ago(2*time.Hour), "knife_karambit", 0, b, 0)
|
||||
event(a, "a1", EventKill, ago(2*time.Hour), "knife", 1, nil, 1) // a bot: doesn't count, not even as a headshot
|
||||
event(b, "b2", EventKill, ago(20*time.Minute), "taser", 0, a, 0)
|
||||
event(b, "b2", EventKill, ago(20*time.Minute), "ak47", 1, a, 0)
|
||||
|
||||
if ok, data, err := st.State(ctx); err != nil || !ok || !data || !st.HasEvents() {
|
||||
t.Fatalf("State with data: %v, %v, %v (events %v)", ok, data, err, st.HasEvents())
|
||||
|
|
@ -194,13 +196,13 @@ func TestPlaytimeDB(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(leaders) != 1 || leaders[0].Name != "Alpha" || leaders[0].T != 1 || leaders[0].CT != 0.5 || leaders[0].Kills != 10 || leaders[0].Deaths != 5 {
|
||||
if len(leaders) != 1 || leaders[0].Name != "Alpha" || leaders[0].T != 1 || leaders[0].CT != 0.5 || leaders[0].Kills != 1 || leaders[0].Deaths != 2 {
|
||||
t.Fatalf("leaders this week %+v", leaders)
|
||||
}
|
||||
if leaders, err = st.Leaders(ctx, nil, 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(leaders) != 2 || leaders[0].Name != "Bravo" || leaders[0].Kills != 3 {
|
||||
if len(leaders) != 2 || leaders[0].Name != "Bravo" || leaders[0].Kills != 2 || leaders[0].Deaths != 1 {
|
||||
t.Fatalf("leaders all time %+v", leaders)
|
||||
}
|
||||
|
||||
|
|
@ -212,19 +214,23 @@ func TestPlaytimeDB(t *testing.T) {
|
|||
for _, d := range p.Days {
|
||||
days += d.Hours
|
||||
}
|
||||
if p.T != 1 || p.CT != 0.5 || p.Spec != 0.5 || p.Stats.Kills != 10 || p.Stats.Rounds != 10 || days != 1.5 || len(p.Maps) != 1 {
|
||||
if p.T != 1 || p.CT != 0.5 || p.Spec != 0.5 || p.Stats == nil || *p.Stats != (Stats{Kills: 1, Deaths: 2}) || days != 1.5 || len(p.Maps) != 1 {
|
||||
t.Fatalf("player %+v, %.1f h in days", p, days)
|
||||
}
|
||||
if p, err := st.Player(ctx, 76561198000009999, now, london); err != nil || p != nil {
|
||||
t.Fatalf("unknown player: %v, %v", p, err)
|
||||
}
|
||||
|
||||
ps, err := st.PlayerStaff(ctx, a, now)
|
||||
ps, err := st.PlayerStaff(ctx, a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Alpha's knife kill was in a1; Bravo killed Alpha twice, but after a1 ended.
|
||||
if r := ps.Recent; len(r) != 1 || r[0].Kills == nil || *r[0].Kills != 1 || r[0].Deaths == nil || *r[0].Deaths != 0 {
|
||||
t.Fatalf("recent session kills and deaths %+v", ps.Recent)
|
||||
}
|
||||
if len(ps.Names) != 2 || ps.Names[0].Name != "Alpha" || len(ps.Recent) != 1 || ps.Recent[0].Seconds != 7200 ||
|
||||
ps.Recent[0].Reason != "NETWORK_DISCONNECT_DISCONNECT_BY_USER" || ps.Country != "GB" || ps.City != "London" || ps.Ping == nil || *ps.Ping != 20 {
|
||||
ps.Recent[0].Reason != "NETWORK_DISCONNECT_DISCONNECT_BY_USER" || ps.Country != "GB" || ps.City != "London" {
|
||||
t.Fatalf("staff %+v", ps)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -338,15 +338,15 @@ function ptDrawer(pt, online) {
|
|||
<h3>Last 30 days</h3>
|
||||
<div class="pt-spark" role="img" aria-label="Hours played each day">${p.days.map((d) => `<i class="${d.hours ? "" : "zero"}" style="height:${d.hours ? Math.max(6, (d.hours / maxDay) * 100) : 4}%" data-tip="${esc(`${ptDay(d.date)} · ${d.hours ? ptH(d.hours) : "didn't play"}`)}"></i>`).join("")}</div>
|
||||
</section>
|
||||
${st.kills + st.deaths > 0 ? `<section>
|
||||
${st && st.kills + st.deaths > 0 ? `<section>
|
||||
<h3>Stats</h3>
|
||||
<div class="pt-stats">
|
||||
<div><b>${ptKD(st.kills, st.deaths)}</b><small>K/D</small></div>
|
||||
<div><b>${ptPct(st.headshots, st.kills)}%</b><small>Headshot kills</small></div>
|
||||
<div><b>${st.rounds ? Math.round(st.damage / st.rounds) : "–"}</b><small>Damage per round</small></div>
|
||||
<div><b>${st.mvps}</b><small>MVPs</small></div>
|
||||
<div><b>${st.kills.toLocaleString()}</b><small>Kills</small></div>
|
||||
<div><b>${st.deaths.toLocaleString()}</b><small>Deaths</small></div>
|
||||
</div>
|
||||
<p class="muted" style="margin:10px 0 0;font-size:14px">${st.kills.toLocaleString()} kills, ${st.deaths.toLocaleString()} deaths and ${st.assists.toLocaleString()} assists over ${st.rounds.toLocaleString()} rounds.</p>
|
||||
<p class="muted" style="margin:10px 0 0;font-size:14px">Against other players only: bots don't count.</p>
|
||||
</section>` : ""}
|
||||
${p.events && Object.values(p.events).some((n) => n > 0) ? `<section>
|
||||
<h3>Records</h3>
|
||||
|
|
@ -356,7 +356,6 @@ function ptDrawer(pt, online) {
|
|||
<div><b>${p.events.knifeKills.toLocaleString()}</b><small>Knife kills</small></div>
|
||||
<div><b>${p.events.zeusKills.toLocaleString()}</b><small>Zeus kills</small></div>
|
||||
</div>
|
||||
<p class="muted" style="margin:10px 0 0;font-size:14px">${p.events.headshots.toLocaleString()} headshot kills. Counted as they happen, not in warmup or on bots.</p>
|
||||
</section>` : ""}
|
||||
${p.maps.length ? `<section>
|
||||
<h3>Favourite maps</h3>
|
||||
|
|
@ -381,17 +380,14 @@ function ptDrawer(pt, online) {
|
|||
<h3>Recent sessions</h3>
|
||||
<ul class="pt-sessions">${s.recent.map((x) => `<li>
|
||||
<div class="top"><b>${fmtDate(x.started)}</b><span>${ptDur(x.seconds)}</span></div>
|
||||
<div class="muted">${esc(x.map)}${x.endMap && x.endMap !== x.map ? ` → ${esc(x.endMap)}` : ""}${x.kills != null ? ` · ${x.kills}–${x.deaths}` : ""}${x.ping != null ? ` · ${x.ping} ms` : ""}</div>
|
||||
<div class="muted">${esc(x.map)}${x.endMap && x.endMap !== x.map ? ` → ${esc(x.endMap)}` : ""}${x.kills != null ? ` · ${x.kills}–${x.deaths}` : ""}</div>
|
||||
<div>${reason(x)}</div>
|
||||
</li>`).join("")}</ul>
|
||||
</section>` : ""}
|
||||
${s.country || s.ping != null ? `<section>
|
||||
<h3>Connection</h3>
|
||||
<dl class="kv">
|
||||
${s.country ? `<dt>Location</dt><dd>${s.city ? `${esc(s.city)}, ` : ""}${esc(s.countryName || s.country)}</dd>` : ""}
|
||||
${s.ping != null ? `<dt>Average ping</dt><dd>${s.ping} ms <span class="muted">last 30 days</span></dd>` : ""}
|
||||
</dl>
|
||||
</section>${s.country ? PT_DBIP : ""}` : ""}`;
|
||||
${s.country ? `<section>
|
||||
<h3>Location</h3>
|
||||
<p style="margin:0">${s.city ? `${esc(s.city)}, ` : ""}${esc(s.countryName || s.country)}</p>
|
||||
</section>${PT_DBIP}` : ""}`;
|
||||
}
|
||||
|
||||
/* ---------- Events ---------- */
|
||||
|
|
|
|||
|
|
@ -36,7 +36,13 @@ html.drawer-open { overflow: hidden; }
|
|||
html.drawer-open body { overflow: hidden; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
/* The server header's 24px grid, across the whole page (fainter: the page is darker than a
|
||||
panel), fixed so content scrolls over it. Panels are solid, so it shows between them. */
|
||||
background:
|
||||
repeating-linear-gradient(0deg, transparent 0 23px, rgba(255, 255, 255, 0.028) 23px 24px),
|
||||
repeating-linear-gradient(90deg, transparent 0 23px, rgba(255, 255, 255, 0.028) 23px 24px),
|
||||
var(--bg);
|
||||
background-attachment: fixed;
|
||||
color: var(--text);
|
||||
font: 400 15px/1.5 var(--body);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ func (s *Server) playerPlaytime(r *http.Request, steamid string, staff bool) map
|
|||
}
|
||||
out := map[string]any{"player": p}
|
||||
if staff {
|
||||
extra, err := s.pt.PlayerStaff(ctx, n, now)
|
||||
extra, err := s.pt.PlayerStaff(ctx, n)
|
||||
if err != nil {
|
||||
slog.Warn("staff playtime for a player", "steamid", steamid, "err", err)
|
||||
return out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue