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
+149 -9
View File
@@ -17,9 +17,11 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
@@ -73,7 +75,7 @@ func boundsToWire(b catalog.Bounds) transport.TrackBounds {
}
}
func trackToWire(t catalog.TrackMeta) transport.TrackWire {
func trackToWire(t catalog.TrackMeta, activeTracks map[string]bool) transport.TrackWire {
return transport.TrackWire{
ID: t.ID,
Name: t.Name,
@@ -92,20 +94,36 @@ func trackToWire(t catalog.TrackMeta) transport.TrackWire {
BestLapHolder: t.BestLapHolder,
CreatedMs: t.CreatedMs,
UpdatedMs: t.UpdatedMs,
IsActive: activeTracks[t.ID],
}
}
func tracksToWire(in []catalog.TrackMeta) []transport.TrackWire {
func tracksToWire(in []catalog.TrackMeta, activeTracks map[string]bool) []transport.TrackWire {
if len(in) == 0 {
return []transport.TrackWire{}
}
out := make([]transport.TrackWire, len(in))
for i, t := range in {
out[i] = trackToWire(t)
out[i] = trackToWire(t, activeTracks)
}
return out
}
func getActiveTracks(ctx context.Context, cat *catalog.Service) map[string]bool {
active := make(map[string]bool)
calendar, err := cat.GetTrackCalendar(ctx)
if err != nil {
return active
}
now := time.Now()
for _, entry := range calendar {
if (now.Equal(entry.StartAt) || now.After(entry.StartAt)) && now.Before(entry.EndAt) {
active[entry.TrackID] = true
}
}
return active
}
func trackFromWire(w transport.TrackWire) catalog.TrackMeta {
return catalog.TrackMeta{
ID: w.ID,
@@ -400,10 +418,11 @@ func mapCatalogError(err error) (int, string) {
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/catalog [get]
func catalogHandler(svc *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
snap := svc.Snapshot()
activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusOK, map[string]any{
"tracks": tracksToWire(snap.Tracks),
"tracks": tracksToWire(snap.Tracks, activeTracks),
"cars": carsToWire(snap.Cars),
"summary": summaryToWire(svc.Stats()),
})
@@ -439,8 +458,9 @@ func tracksHandler(svc *catalog.Service) http.HandlerFunc {
if tag != "" {
out = filterTracksByTag(out, tag)
}
activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusOK, map[string]any{
"tracks": tracksToWire(out),
"tracks": tracksToWire(out, activeTracks),
"count": len(out),
})
}
@@ -471,6 +491,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, http.StatusBadRequest, "bad_request", "missing track id")
return
}
activeTracks := getActiveTracks(r.Context(), svc)
switch r.Method {
case http.MethodGet:
t, ok := svc.GetTrack(id)
@@ -478,7 +499,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, http.StatusNotFound, "not_found", "track not found")
return
}
writeJSON(w, http.StatusOK, trackToWire(t))
writeJSON(w, http.StatusOK, trackToWire(t, activeTracks))
case http.MethodPut:
var req transport.TrackUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -495,7 +516,7 @@ func trackByIDHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, status, code, err.Error())
return
}
writeJSON(w, http.StatusOK, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated)})
writeJSON(w, http.StatusOK, transport.TrackAck{Ok: true, ID: updated.ID, Track: trackToWire(updated, activeTracks)})
case http.MethodDelete:
if err := svc.DeleteTrack(r.Context(), id); err != nil {
status, code := mapCatalogError(err)
@@ -549,7 +570,8 @@ func createTrackHandler(svc *catalog.Service) http.HandlerFunc {
writeError(w, status, code, err.Error())
return
}
writeJSON(w, http.StatusCreated, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created)})
activeTracks := getActiveTracks(r.Context(), svc)
writeJSON(w, http.StatusCreated, transport.TrackAck{Ok: true, ID: created.ID, Track: trackToWire(created, activeTracks)})
}
}
@@ -734,3 +756,121 @@ func carsRouter(svc *catalog.Service) http.HandlerFunc {
}
}
}
// trackCalendarRouter routes /api/tracks/calendar to GET or POST
func trackCalendarRouter(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getTrackCalendarHandler(cat)(w, r)
case http.MethodPost:
createTrackCalendarHandler(cat)(w, r)
default:
w.Header().Set("Allow", "GET, POST")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "use GET or POST")
}
}
}
// getTrackCalendarHandler godoc
// @Summary Get Track Calendar
// @Description Returns the full calendar of track active periods.
// @Tags tracks
// @Produce json
// @Success 200 {object} transport.TrackCalendarResponse "List of scheduled active periods"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar [get]
func getTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
entries, err := cat.GetTrackCalendar(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.TrackCalendarEntry, len(entries))
for i, entry := range entries {
items[i] = transport.TrackCalendarEntry{
ID: entry.ID,
TrackID: entry.TrackID,
StartAt: entry.StartAt,
EndAt: entry.EndAt,
}
}
writeJSON(w, http.StatusOK, transport.TrackCalendarResponse{Items: items})
}
}
// createTrackCalendarHandler godoc
// @Summary Schedule Track Active Period
// @Description Schedules an active period for a physical track.
// @Tags tracks
// @Accept json
// @Produce json
// @Param payload body transport.TrackCalendarCreateRequest true "Calendar entry details"
// @Success 201 {object} transport.TrackCalendarEntry "Created calendar entry"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 404 {object} map[string]interface{} "Track not found"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar [post]
func createTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req transport.TrackCalendarCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
entry, err := cat.CreateTrackCalendar(r.Context(), req.TrackID, req.StartAt, req.EndAt)
if err != nil {
if errors.Is(err, catalog.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", err.Error())
return
}
if errors.Is(err, catalog.ErrInvalidInput) {
writeError(w, http.StatusBadRequest, "invalid_input", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
writeJSON(w, http.StatusCreated, transport.TrackCalendarEntry{
ID: entry.ID,
TrackID: entry.TrackID,
StartAt: entry.StartAt,
EndAt: entry.EndAt,
})
}
}
// deleteTrackCalendarHandler godoc
// @Summary Delete Scheduled Period
// @Description Deletes a scheduled active period by ID.
// @Tags tracks
// @Param id path int true "Calendar Entry ID"
// @Success 204 "No Content"
// @Failure 404 {object} map[string]interface{} "Not found"
// @Failure 500 {object} map[string]interface{} "Internal error"
// @Router /api/tracks/calendar/{id} [delete]
func deleteTrackCalendarHandler(cat *catalog.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/api/tracks/calendar/")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id", "id must be an integer")
return
}
err = cat.DeleteTrackCalendar(r.Context(), id)
if err != nil {
if errors.Is(err, catalog.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
}