diff --git a/README.md b/README.md index 89d58a4..677d352 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ when `SAW_TEST_PT_DSN` is set. It drops and recreates PlaytimeHistory's tables f give it a scratch database: ``` -podman run -d --rm --name pt-test -e MARIADB_ROOT_PASSWORD=test -e MARIADB_DATABASE=playtime_history -p 127.0.0.1:33071:3306 mariadb:11 +podman run -d --rm --name pt-test -e MARIADB_ROOT_PASSWORD=test -e MARIADB_DATABASE=playtime_history -p 127.0.0.1:33071:3306 mariadb:10.6 SAW_TEST_PT_DSN='root:test@tcp(127.0.0.1:33071)/playtime_history' go test ./internal/playtime -run DB ``` diff --git a/internal/playtime/playtime.go b/internal/playtime/playtime.go index 301f8da..a69a6cd 100644 --- a/internal/playtime/playtime.go +++ b/internal/playtime/playtime.go @@ -233,8 +233,12 @@ 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 - Days []Day `json:"days"` - Maps []MapTime `json:"maps"` + // 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"` + HeatDays int `json:"heatDays"` + Days []Day `json:"days"` + Maps []MapTime `json:"maps"` } func (s *Store) Overview(ctx context.Context, now time.Time, loc *time.Location) (Overview, error) { @@ -246,12 +250,38 @@ func (s *Store) Overview(ctx context.Context, now time.Time, loc *time.Location) } week, fortnight := now.Add(-7*24*time.Hour), now.Add(-14*24*time.Hour) heatFrom := dayStart(now, loc).AddDate(0, 0, -27) + + // Average over the days there's been recording for, not days from before it began. + where, args := s.and("server_id", nil) + var first sql.NullTime + if err := s.db.QueryRowContext(ctx, `SELECT MIN(started_at) FROM pt_sessions WHERE 1=1`+where, args...).Scan(&first); err != nil { + return o, err + } + byHourFrom, heatStart := start, heatFrom + if first.Valid { + if f := dayStart(first.Time, loc); f.After(byHourFrom) { + byHourFrom = f + } + if f := dayStart(first.Time, loc); f.After(heatStart) { + heatStart = f + } + } + today := dayStart(now, loc) + var weekdays [7]int // how many of each weekday the heatmap covers + for d := heatStart; !d.After(today); d = d.AddDate(0, 0, 1) { + weekdays[(int(d.Weekday())+6)%7]++ + o.HeatDays++ + } + for d := byHourFrom; !d.After(today); d = d.AddDate(0, 0, 1) { + o.ByHourDays++ + } + for _, b := range buckets { h := float64(b.Seconds) / 3600 local := b.Start.In(loc) - o.ByHour[local.Hour()] += h / 30 - if !local.Before(heatFrom) { - o.Heat[(int(local.Weekday())+6)%7][local.Hour()] += h / 4 + 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): @@ -271,7 +301,7 @@ func (s *Store) Overview(ctx context.Context, now time.Time, loc *time.Location) } o.Summary.Hours, o.Summary.HoursPrev = round1(o.Summary.Hours), round1(o.Summary.HoursPrev) - where, args := s.and("server_id", []any{week, fortnight, week, fortnight}) + where, args = s.and("server_id", []any{week, fortnight, week, fortnight}) if err := s.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT IF(started_at >= ?, steamid64, NULL)), COUNT(DISTINCT IF(started_at >= ? AND started_at < ?, steamid64, NULL)) diff --git a/internal/playtime/playtime_db_test.go b/internal/playtime/playtime_db_test.go index 016f375..371c06c 100644 --- a/internal/playtime/playtime_db_test.go +++ b/internal/playtime/playtime_db_test.go @@ -170,8 +170,12 @@ func TestPlaytimeDB(t *testing.T) { for _, h := range o.ByHour { byHour += h } - if byHour < 3.3/30 || byHour > 3.7/30 { // 3.5 h spread over 30 days, rounded per hour - t.Fatalf("byHour adds up to %.3f, want about %.3f", byHour, 3.5/30) + // Recording began with Bravo's session 10 days ago, so 3.5 h spread over those 11 days. + 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) } // Alpha on one day, Bravo on two days ten days apart. players := 0 diff --git a/internal/web/assets/playtime.js b/internal/web/assets/playtime.js index badac54..cd514e5 100644 --- a/internal/web/assets/playtime.js +++ b/internal/web/assets/playtime.js @@ -30,8 +30,10 @@ const PT_RECORDS = [ ]; const ptH = (h) => (h >= 100 ? Math.round(h).toLocaleString() : (Math.round(h * 10) / 10).toFixed(1)) + " h"; -const ptPct = (a, b) => Math.round((a / Math.max(b, 1)) * 100); -const ptKD = (k, d) => (k / Math.max(d, 1)).toFixed(2); +// a as a percentage of b. Works for hours under 1 too; nothing of nothing is 0. +const ptPct = (a, b) => (b > 0 ? Math.round((a / b) * 100) : 0); +// "–" until there's a kill or death to go on (session stats are saved when a session ends). +const ptKD = (k, d) => (k + d > 0 ? (k / Math.max(d, 1)).toFixed(2) : "–"); const ptPad = (h) => String(h).padStart(2, "0"); const ptDur = (s) => (s >= 3600 ? `${Math.floor(s / 3600)}h ${ptPad(Math.floor((s % 3600) / 60))}m` : `${Math.max(1, Math.round(s / 60))}m`); const ptDay = (iso, opts = { weekday: "short", day: "numeric", month: "short" }) => new Date(`${iso}T12:00:00`).toLocaleDateString(undefined, opts); @@ -84,7 +86,7 @@ function ptTiles(s) { const delta = (a, b) => (b ? `${a >= b ? "+" : ""}${ptPct(a - b, b)}% on the 7 days before` : 'Nothing the 7 days before'); return `
Players on T or CT, averaged over the last 30 days, in your time zone.
Players on T or CT (bots don't count), averaged over ${ptDaysText(o.byHourDays)}, in your time zone.
Average players on T or CT by day and hour, last 4 weeks, in your time zone.
Average players on T or CT by day and hour, over ${ptDaysText(o.heatDays)}, in your time zone.
| ${o.heat[0].map((_, h) => ` | ${h % 3 === 0 ? ptPad(h) : ""} | `).join("")}
|---|---|
| ${PT_WEEKDAYS[wd]} | ${row.map((v, h) => ``).join("")} |