Add leaderboard for all tracks and track calendar

This commit is contained in:
2026-07-15 11:53:35 +04:00
parent 4a894a4399
commit a56841237d
20 changed files with 1394 additions and 25 deletions
+44
View File
@@ -451,3 +451,47 @@ func (s *PgStore) GetDriverLeaderboardProfile(ctx context.Context, driverID stri
}
return &entry, nil
}
// TrackCalendarInfo holds the basic details of a scheduled track.
type TrackCalendarInfo struct {
ID int
TrackID string
TrackName string
StartAt time.Time
EndAt time.Time
}
// TrackCalendarLeaderboard holds the leaderboard summary for a track calendar event.
type TrackCalendarLeaderboard struct {
TrackID string
TrackName string
StartAt time.Time
EndAt time.Time
Status string // finished, active, planned
Podium []LeaderboardEntry
CurrentDriver *LeaderboardEntry
}
// GetCalendarLeaderboardTracks returns all track calendar entries joined with their track names, sorted chronologically.
func (s *PgStore) GetCalendarLeaderboardTracks(ctx context.Context) ([]TrackCalendarInfo, error) {
rows, err := s.pool.Query(ctx, `
SELECT tc.id, tc.track_id, t.name, tc.start_at, tc.end_at
FROM track_calendar tc
JOIN tracks t ON tc.track_id = t.id
ORDER BY tc.start_at DESC
`)
if err != nil {
return nil, fmt.Errorf("query calendar tracks: %w", err)
}
defer rows.Close()
var out []TrackCalendarInfo
for rows.Next() {
var info TrackCalendarInfo
if err := rows.Scan(&info.ID, &info.TrackID, &info.TrackName, &info.StartAt, &info.EndAt); err != nil {
return nil, fmt.Errorf("scan calendar track: %w", err)
}
out = append(out, info)
}
return out, nil
}
+48
View File
@@ -259,4 +259,52 @@ func TestLeaderboard(t *testing.T) {
t.Errorf("expected Driver One, DRV, TST, got %+v", entry)
}
})
// Test Calendar Leaderboard
t.Run("Calendar Leaderboard", func(t *testing.T) {
_, _ = pool.Exec(ctx, "DELETE FROM track_calendar")
now := time.Now()
_, err = pool.Exec(ctx, `
INSERT INTO track_calendar (track_id, start_at, end_at) VALUES
('monaco', $1, $2),
('barcelona-catalunya', $3, $4),
('austria-redbull-ring', $5, $6)
`,
now.Add(-10*time.Hour), now.Add(-5*time.Hour),
now.Add(-2*time.Hour), now.Add(2*time.Hour),
now.Add(5*time.Hour), now.Add(10*time.Hour),
)
if err != nil {
t.Fatalf("failed to insert track calendar: %v", err)
}
defer func() {
_, _ = pool.Exec(ctx, "DELETE FROM track_calendar")
}()
svc := NewService(store, nil)
svc.now = func() time.Time { return now }
res, err := svc.GetCalendarLeaderboard(ctx, currentYear, "test-driver-1")
if err != nil {
t.Fatalf("failed to get calendar leaderboard: %v", err)
}
if len(res) != 3 {
t.Fatalf("expected 3 calendar entries, got %d", len(res))
}
// 1. Monaco (finished)
if res[0].TrackID != "monaco" || res[0].Status != "finished" {
t.Errorf("expected monaco (finished), got %s (%s)", res[0].TrackID, res[0].Status)
}
// 2. Barcelona (active)
if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" {
t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status)
}
// 3. Austria (planned)
if res[2].TrackID != "austria-redbull-ring" || res[2].Status != "planned" {
t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[2].TrackID, res[2].Status)
}
})
}
+62 -5
View File
@@ -236,10 +236,54 @@ func (r *Runner) Run(ctx context.Context, opt Options) (Summary, error) {
s.PlansInserted++
}
if err := r.seedTrackCalendar(ctx); err != nil {
return s, fmt.Errorf("seed track calendar: %w", err)
}
s.Duration = time.Since(start)
return s, nil
}
func (r *Runner) seedTrackCalendar(ctx context.Context) error {
now := r.now
if len(r.trackIDs) == 0 {
return nil
}
tracksMap := make(map[string]bool)
for _, id := range r.trackIDs {
tracksMap[id] = true
}
schedule := []struct {
trackID string
startAt time.Time
endAt time.Time
}{
{"monaco", now.Add(-14 * 24 * time.Hour), now.Add(-7 * 24 * time.Hour)},
{"barcelona-catalunya", now.Add(-2 * 24 * time.Hour), now.Add(12 * 24 * time.Hour)},
{"austria-redbull-ring", now.Add(14 * 24 * time.Hour), now.Add(28 * 24 * time.Hour)},
{"great-britain-silverstone", now.Add(28 * 24 * time.Hour), now.Add(42 * 24 * time.Hour)},
{"belgium-spa", now.Add(42 * 24 * time.Hour), now.Add(56 * 24 * time.Hour)},
}
for _, s := range schedule {
if !tracksMap[s.trackID] {
continue
}
_, err := r.pg.Exec(ctx, `
INSERT INTO track_calendar (track_id, start_at, end_at)
VALUES ($1, $2, $3)
`, s.trackID, s.startAt, s.endAt)
if err != nil {
return fmt.Errorf("insert calendar entry: %w", err)
}
}
return nil
}
type planShape struct {
intervalS int
count int
@@ -251,18 +295,31 @@ type planShape struct {
// cascade), plus the plan and queue tables. Drivers and clans are
// kept (they're independent of races).
func (r *Runner) resetAll(ctx context.Context) error {
if _, err := r.pg.Exec(ctx,
`DELETE FROM races WHERE status IN ('finished','cancelled')`); err != nil {
if _, err := r.pg.Exec(ctx, `DELETE FROM race_queue`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx,
`TRUNCATE TABLE race_plans, race_queue, drivers, clans CASCADE`); err != nil {
if _, err := r.pg.Exec(ctx, `DELETE FROM races`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM race_plans`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM track_calendar`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM drivers`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `DELETE FROM clans`); err != nil {
return err
}
if _, err := r.pg.Exec(ctx, `ALTER SEQUENCE IF EXISTS track_calendar_id_seq RESTART WITH 1`); err != nil {
return err
}
for _, m := range r.lb.ListRaces() {
_ = r.lb.DeleteRace(m.ID)
}
if _, err := r.pg.Exec(ctx, `TRUNCATE TABLE lobby_drivers`); err != nil {
if _, err := r.pg.Exec(ctx, `DELETE FROM lobby_drivers`); err != nil {
return err
}
return nil
+72
View File
@@ -740,6 +740,78 @@ func (s *Service) GetLeaderboard(ctx context.Context, trackID string, year int,
}, nil
}
// GetCalendarLeaderboard returns the leaderboard details of all scheduled tracks.
func (s *Service) GetCalendarLeaderboard(ctx context.Context, year int, currentDriverID string) ([]TrackCalendarLeaderboard, error) {
if year == 0 {
year = s.now().Year()
}
calendarInfo, err := s.pg.GetCalendarLeaderboardTracks(ctx)
if err != nil {
return nil, err
}
now := s.now()
var out []TrackCalendarLeaderboard
// Cache driver profile fallback if we need it
var fallbackProfile *LeaderboardEntry
if currentDriverID != "" {
fallbackProfile, err = s.pg.GetDriverLeaderboardProfile(ctx, currentDriverID)
if err != nil {
return nil, err
}
if fallbackProfile != nil {
fallbackProfile.Points = 0
fallbackProfile.Rank = 0
}
}
for _, cal := range calendarInfo {
status := "planned"
if now.After(cal.EndAt) {
status = "finished"
} else if (now.Equal(cal.StartAt) || now.After(cal.StartAt)) && now.Before(cal.EndAt) {
status = "active"
}
// Get top 3
podium, _, err := s.pg.GetTrackLeaderboard(ctx, cal.TrackID, year, 3, 0)
if err != nil {
return nil, err
}
if podium == nil {
podium = []LeaderboardEntry{}
}
// Get current driver
var curDriver *LeaderboardEntry
if currentDriverID != "" {
curDriver, err = s.pg.GetTrackLeaderboardDriver(ctx, cal.TrackID, year, currentDriverID)
if err != nil {
return nil, err
}
if curDriver == nil && fallbackProfile != nil {
// Clone the fallback profile so we don't share pointer/modify original
profileCopy := *fallbackProfile
curDriver = &profileCopy
}
}
out = append(out, TrackCalendarLeaderboard{
TrackID: cal.TrackID,
TrackName: cal.TrackName,
StartAt: cal.StartAt,
EndAt: cal.EndAt,
Status: status,
Podium: podium,
CurrentDriver: curDriver,
})
}
return out, nil
}
// ---------------------------------------------------------------------------
// Scheduler
// ---------------------------------------------------------------------------
+2 -2
View File
@@ -112,8 +112,8 @@ func NewPgStore(pool *pgxpool.Pool) *PgStore {
}
// Exec exposes a raw Exec for the seeder / maintenance scripts.
func (s *PgStore) Exec(ctx context.Context, sql string) (int64, error) {
tag, err := s.pool.Exec(ctx, sql)
func (s *PgStore) Exec(ctx context.Context, sql string, args ...any) (int64, error) {
tag, err := s.pool.Exec(ctx, sql, args...)
if err != nil {
return 0, err
}