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
+41
View File
@@ -606,4 +606,45 @@ func (s *Service) ListCarsByOwner(ownerID string) []CarMeta {
}
}
return out
}
// GetTrackCalendar returns all track calendar entries from the store.
func (s *Service) GetTrackCalendar(ctx context.Context) ([]TrackCalendarEntry, error) {
if s.store == nil {
return nil, ErrStoreMissing
}
return s.store.GetTrackCalendar(ctx)
}
// CreateTrackCalendar creates a new calendar entry.
func (s *Service) CreateTrackCalendar(ctx context.Context, trackID string, startAt, endAt time.Time) (TrackCalendarEntry, error) {
if s.store == nil {
return TrackCalendarEntry{}, ErrStoreMissing
}
s.mu.RLock()
_, trackExists := s.tracks[TrackID(trackID)]
s.mu.RUnlock()
if !trackExists {
return TrackCalendarEntry{}, fmt.Errorf("%w: track %s does not exist", ErrNotFound, trackID)
}
if startAt.After(endAt) || startAt.Equal(endAt) {
return TrackCalendarEntry{}, fmt.Errorf("%w: start time must be before end time", ErrInvalidInput)
}
entry := TrackCalendarEntry{
TrackID: trackID,
StartAt: startAt,
EndAt: endAt,
}
return s.store.CreateTrackCalendar(ctx, entry)
}
// DeleteTrackCalendar deletes a calendar entry.
func (s *Service) DeleteTrackCalendar(ctx context.Context, id int) error {
if s.store == nil {
return ErrStoreMissing
}
return s.store.DeleteTrackCalendar(ctx, id)
}
+50
View File
@@ -398,4 +398,54 @@ func TestServiceWithoutStore(t *testing.T) {
if err == nil {
t.Fatal("expected error when store is nil")
}
}
func TestTrackCalendar(t *testing.T) {
s, _ := loadSeeds()
defer s.Stop()
ctx := context.Background()
// 1. Initially calendar should be empty
entries, err := s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries, got %d", len(entries))
}
// 2. Schedule a track
start := time.Now().Add(-1 * time.Hour)
end := time.Now().Add(1 * time.Hour)
created, err := s.CreateTrackCalendar(ctx, "monaco", start, end)
if err != nil {
t.Fatalf("create calendar: %v", err)
}
if created.TrackID != "monaco" || !created.StartAt.Equal(start) || !created.EndAt.Equal(end) {
t.Errorf("invalid created entry: %+v", created)
}
// 3. Get calendar should return the entry
entries, err = s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 1 || entries[0].ID != created.ID {
t.Errorf("expected 1 entry with id %d, got %v", created.ID, entries)
}
// 4. Delete entry
err = s.DeleteTrackCalendar(ctx, created.ID)
if err != nil {
t.Fatalf("delete calendar: %v", err)
}
// 5. Get calendar should be empty again
entries, err = s.GetTrackCalendar(ctx)
if err != nil {
t.Fatalf("get calendar: %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries, got %d", len(entries))
}
}
+5
View File
@@ -27,4 +27,9 @@ type Store interface {
// UpdateCarStats applies a partial mutation of read-mostly stats
// fields on a car. Used by the race engine hooks.
UpdateCarStats(ctx context.Context, id string, fn func(*CarMeta)) error
// Track Calendar methods
GetTrackCalendar(ctx context.Context) ([]TrackCalendarEntry, error)
CreateTrackCalendar(ctx context.Context, entry TrackCalendarEntry) (TrackCalendarEntry, error)
DeleteTrackCalendar(ctx context.Context, id int) error
}
+34 -3
View File
@@ -8,15 +8,18 @@ import (
// memoryStore is an in-process implementation of Store for tests and
// for early PoC runs without a database. It is goroutine-safe.
type memoryStore struct {
mu sync.Mutex
tracks map[TrackID]TrackMeta
cars map[CarID]CarMeta
mu sync.Mutex
tracks map[TrackID]TrackMeta
cars map[CarID]CarMeta
calendar []TrackCalendarEntry
nextID int
}
func newMemoryStore() *memoryStore {
return &memoryStore{
tracks: make(map[TrackID]TrackMeta),
cars: make(map[CarID]CarMeta),
nextID: 1,
}
}
@@ -72,4 +75,32 @@ func (m *memoryStore) UpdateCarStats(_ context.Context, id string, fn func(*CarM
fn(&c)
m.cars[id] = c
return nil
}
func (m *memoryStore) GetTrackCalendar(_ context.Context) ([]TrackCalendarEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := append([]TrackCalendarEntry(nil), m.calendar...)
return out, nil
}
func (m *memoryStore) CreateTrackCalendar(_ context.Context, entry TrackCalendarEntry) (TrackCalendarEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
entry.ID = m.nextID
m.nextID++
m.calendar = append(m.calendar, entry)
return entry, nil
}
func (m *memoryStore) DeleteTrackCalendar(_ context.Context, id int) error {
m.mu.Lock()
defer m.mu.Unlock()
for i, entry := range m.calendar {
if entry.ID == id {
m.calendar = append(m.calendar[:i], m.calendar[i+1:]...)
return nil
}
}
return ErrNotFound
}
+8
View File
@@ -94,6 +94,14 @@ type TrackMeta struct {
UpdatedMs int64 `json:"updated_ms"`
}
// TrackCalendarEntry describes one active period for a track.
type TrackCalendarEntry struct {
ID int `json:"id"`
TrackID string `json:"track_id"`
StartAt time.Time `json:"start_at"`
EndAt time.Time `json:"end_at"`
}
// MotorSpec describes the electric motor.
type MotorSpec struct {
Kind string `json:"kind"` // brushed | brushless