Compare commits
8 Commits
v0.12.0-de
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
| 1100072245 | |||
| 49dfa38475 | |||
| 747adc77a5 | |||
| c9a32e8889 | |||
| 7d470f3638 | |||
| cf7473b373 | |||
| 35c53cc54c | |||
| 016824e846 |
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/middleware"
|
"acc-server-manager/local/middleware"
|
||||||
|
"acc-server-manager/local/model"
|
||||||
"acc-server-manager/local/service"
|
"acc-server-manager/local/service"
|
||||||
"acc-server-manager/local/utl/common"
|
"acc-server-manager/local/utl/common"
|
||||||
"acc-server-manager/local/utl/error_handler"
|
"acc-server-manager/local/utl/error_handler"
|
||||||
@@ -34,6 +35,7 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro
|
|||||||
|
|
||||||
configGroup := routeGroups.Config
|
configGroup := routeGroups.Config
|
||||||
configGroup.Use(auth.Authenticate)
|
configGroup.Use(auth.Authenticate)
|
||||||
|
configGroup.Patch("/", ac.UpdateAllConfigs)
|
||||||
configGroup.Put("/:file", ac.UpdateConfig)
|
configGroup.Put("/:file", ac.UpdateConfig)
|
||||||
configGroup.Get("/:file", ac.GetConfig)
|
configGroup.Get("/:file", ac.GetConfig)
|
||||||
configGroup.Get("/", ac.GetConfigs)
|
configGroup.Get("/", ac.GetConfigs)
|
||||||
@@ -41,6 +43,54 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro
|
|||||||
return ac
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAllConfigs updates all configuration files
|
||||||
|
//
|
||||||
|
// @Summary Bulk update all server configuration files
|
||||||
|
// @Description Update multiple ACC server configuration files in one request. Null or omitted sections are skipped.
|
||||||
|
// @Tags Server Configuration
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Server ID (UUID format)"
|
||||||
|
// @Param restart query bool false "Restart the server after update"
|
||||||
|
// @Param override query bool false "Override instead of merging with existing config"
|
||||||
|
// @Param content body model.ConfigurationsUpdateRequest true "Configuration sections to update (null sections are skipped)"
|
||||||
|
// @Success 200 {array} model.Config "List of updated config audit records"
|
||||||
|
// @Failure 400 {object} error_handler.ErrorResponse "Invalid request or JSON format"
|
||||||
|
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 403 {object} error_handler.ErrorResponse "Insufficient permissions"
|
||||||
|
// @Failure 404 {object} error_handler.ErrorResponse "Server not found"
|
||||||
|
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /server/{id}/config [patch]
|
||||||
|
func (ac *ConfigController) UpdateAllConfigs(c *fiber.Ctx) error {
|
||||||
|
restart := c.QueryBool("restart")
|
||||||
|
serverID := c.Params("id")
|
||||||
|
|
||||||
|
if _, err := uuid.Parse(serverID); err != nil {
|
||||||
|
return ac.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("serverId", serverID)
|
||||||
|
|
||||||
|
var req model.ConfigurationsUpdateRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return ac.errorHandler.HandleParsingError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := ac.service.UpdateAllConfigs(c, &req)
|
||||||
|
if err != nil {
|
||||||
|
return ac.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if restart {
|
||||||
|
if _, err := ac.apiService.ServiceControlRestartServer(c); err != nil {
|
||||||
|
logging.ErrorWithContext("CONFIG_RESTART", "Failed to restart server after bulk config update: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(configs)
|
||||||
|
}
|
||||||
|
|
||||||
// updateConfig returns Config
|
// updateConfig returns Config
|
||||||
//
|
//
|
||||||
// @Summary Update server configuration file
|
// @Summary Update server configuration file
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ func NewLeaderboardController(ls *service.LeaderboardService, routeGroups *commo
|
|||||||
errorHandler: error_handler.NewControllerErrorHandler(),
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiServerRoutes := routeGroups.Api.Group("/server/:id")
|
||||||
|
apiServerRoutes.Get("/leaderboard", lc.Get)
|
||||||
|
|
||||||
routeGroups.Leaderboard.Get("/", lc.Get)
|
routeGroups.Leaderboard.Get("/", lc.Get)
|
||||||
routeGroups.Leaderboard.Put("/", auth.Authenticate, lc.Update)
|
routeGroups.Leaderboard.Put("/", auth.Authenticate, lc.Update)
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ func (lc *LeaderboardController) Update(c *fiber.Ctx) error {
|
|||||||
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
var input model.LeaderboardInput
|
var input model.Leaderboard
|
||||||
if err := c.BodyParser(&input); err != nil {
|
if err := c.BodyParser(&input); err != nil {
|
||||||
return lc.errorHandler.HandleParsingError(c, err)
|
return lc.errorHandler.HandleParsingError(c, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ type Configurations struct {
|
|||||||
Settings ServerSettings `json:"settings"`
|
Settings ServerSettings `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigurationsUpdateRequest struct {
|
||||||
|
Configuration *map[string]interface{} `json:"configuration"`
|
||||||
|
AssistRules *map[string]interface{} `json:"assistRules"`
|
||||||
|
Event *map[string]interface{} `json:"event"`
|
||||||
|
EventRules *map[string]interface{} `json:"eventRules"`
|
||||||
|
Settings *map[string]interface{} `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerSettings struct {
|
type ServerSettings struct {
|
||||||
ServerName string `json:"serverName"`
|
ServerName string `json:"serverName"`
|
||||||
AdminPassword string `json:"adminPassword"`
|
AdminPassword string `json:"adminPassword"`
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Score is a custom type that accepts both numbers and strings in JSON.
|
||||||
|
type Score string
|
||||||
|
|
||||||
|
func (s *Score) UnmarshalJSON(data []byte) error {
|
||||||
|
var n json.Number
|
||||||
|
if err := json.Unmarshal(data, &n); err == nil {
|
||||||
|
*s = Score(n.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(data, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = Score(str)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Score) MarshalJSON() ([]byte, error) {
|
||||||
|
if n, err := strconv.Atoi(string(s)); err == nil {
|
||||||
|
return json.Marshal(n)
|
||||||
|
}
|
||||||
|
return json.Marshal(string(s))
|
||||||
|
}
|
||||||
|
|
||||||
type Leaderboard struct {
|
type Leaderboard struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
ServerID uuid.UUID `gorm:"uniqueIndex;not null;type:uuid" json:"serverId"`
|
ServerID uuid.UUID `gorm:"uniqueIndex;not null;type:uuid" json:"serverId"`
|
||||||
@@ -12,38 +38,38 @@ type Leaderboard struct {
|
|||||||
FLColor string `gorm:"default:'#8b5cf6'" json:"flColor"`
|
FLColor string `gorm:"default:'#8b5cf6'" json:"flColor"`
|
||||||
FLTextColor string `gorm:"default:'#000000'" json:"flTextColor"`
|
FLTextColor string `gorm:"default:'#000000'" json:"flTextColor"`
|
||||||
Drivers []LeaderboardDriver `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"drivers"`
|
Drivers []LeaderboardDriver `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"drivers"`
|
||||||
Races []LeaderboardRace `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"races"`
|
Races []LeaderboardRace `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"tracks"`
|
||||||
PointRows []LeaderboardPointRow `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"pointRows"`
|
PointRows []LeaderboardPointRow `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"pointsTable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardDriver struct {
|
type LeaderboardDriver struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Initials string `json:"initials"`
|
Initials string `json:"initials"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
Position int `json:"position"`
|
Position int `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardRace struct {
|
type LeaderboardRace struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Position int `json:"position"`
|
Position int `json:"-"`
|
||||||
FastestLapInitials string `json:"fastestLapInitials"`
|
FastestLapDriverID *uuid.UUID `gorm:"type:uuid" json:"fastestLapDriverId"`
|
||||||
Results []LeaderboardResult `gorm:"foreignKey:RaceID;constraint:OnDelete:CASCADE" json:"results"`
|
Results []LeaderboardResult `gorm:"foreignKey:RaceID;constraint:OnDelete:CASCADE" json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardResult struct {
|
type LeaderboardResult struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"-"`
|
||||||
RaceID uuid.UUID `gorm:"not null;type:uuid" json:"raceId"`
|
RaceID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
DriverID uuid.UUID `gorm:"not null;type:uuid" json:"driverId"`
|
DriverID uuid.UUID `gorm:"not null;type:uuid" json:"driverId"`
|
||||||
Score string `json:"score"`
|
Score Score `json:"score"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardPointRow struct {
|
type LeaderboardPointRow struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Points int `json:"points"`
|
Points int `json:"points"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
@@ -51,42 +77,6 @@ type LeaderboardPointRow struct {
|
|||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaderboardInput is the wire format matching the HTML's appData shape.
|
|
||||||
type LeaderboardInput struct {
|
|
||||||
Drivers []LeaderboardDriverInput `json:"drivers"`
|
|
||||||
PointsTable []LeaderboardPointRowInput `json:"pointsTable"`
|
|
||||||
FLPoints LeaderboardFLInput `json:"flPoints"`
|
|
||||||
Tracks []LeaderboardTrackInput `json:"tracks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardDriverInput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Initials string `json:"initials"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardPointRowInput struct {
|
|
||||||
Points int `json:"points"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
TextColor string `json:"textColor"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardFLInput struct {
|
|
||||||
Points int `json:"points"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
TextColor string `json:"textColor"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardTrackInput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Results []interface{} `json:"results"`
|
|
||||||
FastestLapInitials string `json:"fastestLapInitials"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Leaderboard) BeforeCreate(tx *gorm.DB) error {
|
func (l *Leaderboard) BeforeCreate(tx *gorm.DB) error {
|
||||||
if l.ID == uuid.Nil {
|
if l.ID == uuid.Nil {
|
||||||
l.ID = uuid.New()
|
l.ID = uuid.New()
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ func (r *LeaderboardRepository) FullReplace(ctx context.Context, serverID uuid.U
|
|||||||
// Re-insert children with correct leaderboard ID
|
// Re-insert children with correct leaderboard ID
|
||||||
for i := range lb.Drivers {
|
for i := range lb.Drivers {
|
||||||
lb.Drivers[i].LeaderboardID = existing.ID
|
lb.Drivers[i].LeaderboardID = existing.ID
|
||||||
lb.Drivers[i].ID = uuid.Nil // force new ID
|
|
||||||
}
|
}
|
||||||
if len(lb.Drivers) > 0 {
|
if len(lb.Drivers) > 0 {
|
||||||
if err := tx.Create(&lb.Drivers).Error; err != nil {
|
if err := tx.Create(&lb.Drivers).Error; err != nil {
|
||||||
@@ -112,7 +111,6 @@ func (r *LeaderboardRepository) FullReplace(ctx context.Context, serverID uuid.U
|
|||||||
|
|
||||||
for i := range lb.Races {
|
for i := range lb.Races {
|
||||||
lb.Races[i].LeaderboardID = existing.ID
|
lb.Races[i].LeaderboardID = existing.ID
|
||||||
lb.Races[i].ID = uuid.Nil
|
|
||||||
}
|
}
|
||||||
if len(lb.Races) > 0 {
|
if len(lb.Races) > 0 {
|
||||||
// Create races first (without results) to get IDs
|
// Create races first (without results) to get IDs
|
||||||
|
|||||||
@@ -91,6 +91,72 @@ func (as *ConfigService) SetServerService(serverService *ServerService) {
|
|||||||
// context.Context: Application context
|
// context.Context: Application context
|
||||||
// Returns:
|
// Returns:
|
||||||
// string: Application version
|
// string: Application version
|
||||||
|
func (as *ConfigService) UpdateAllConfigs(ctx *fiber.Ctx, req *model.ConfigurationsUpdateRequest) ([]*model.Config, error) {
|
||||||
|
serverID := ctx.Locals("serverId").(string)
|
||||||
|
override := ctx.QueryBool("override", false)
|
||||||
|
|
||||||
|
serverUUID, err := uuid.Parse(serverID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Invalid server ID format: %v", err)
|
||||||
|
return nil, fmt.Errorf("invalid server ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverUUID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Server not found")
|
||||||
|
return nil, fmt.Errorf("server not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type configEntry struct {
|
||||||
|
fileName string
|
||||||
|
body *map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := []configEntry{
|
||||||
|
{ConfigurationJson, req.Configuration},
|
||||||
|
{AssistRulesJson, req.AssistRules},
|
||||||
|
{EventJson, req.Event},
|
||||||
|
{EventRulesJson, req.EventRules},
|
||||||
|
{SettingsJson, req.Settings},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []*model.Config
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.body == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oldDataUTF8, newData, err := as.updateConfigFiles(ctx.UserContext(), server, entry.fileName, entry.body, override)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry.fileName == SettingsJson {
|
||||||
|
if nameVal, ok := (*entry.body)["serverName"]; ok {
|
||||||
|
if nameStr, ok := nameVal.(string); ok && nameStr != "" && nameStr != server.Name {
|
||||||
|
server.Name = nameStr
|
||||||
|
if err := as.serverRepository.Update(ctx.UserContext(), server); err != nil {
|
||||||
|
logging.Error("Failed to update server name in DB: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := as.repository.UpdateConfig(ctx.UserContext(), &model.Config{
|
||||||
|
ServerID: serverUUID,
|
||||||
|
ConfigFile: entry.fileName,
|
||||||
|
OldConfig: string(oldDataUTF8),
|
||||||
|
NewConfig: string(newData),
|
||||||
|
ChangedAt: time.Now(),
|
||||||
|
})
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
as.configCache.InvalidateServerCache(serverID)
|
||||||
|
as.serverService.StartAccServerRuntime(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
|
func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
|
||||||
serverID := ctx.Locals("serverId").(string)
|
serverID := ctx.Locals("serverId").(string)
|
||||||
configFile := ctx.Params("file")
|
configFile := ctx.Params("file")
|
||||||
@@ -173,6 +239,17 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID stri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configFile == SettingsJson {
|
||||||
|
if nameVal, ok := (*body)["serverName"]; ok {
|
||||||
|
if nameStr, ok := nameVal.(string); ok && nameStr != "" && nameStr != server.Name {
|
||||||
|
server.Name = nameStr
|
||||||
|
if err := as.serverRepository.Update(ctx, server); err != nil {
|
||||||
|
logging.Error("Failed to update server name in DB: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
as.configCache.InvalidateServerCache(serverID)
|
as.configCache.InvalidateServerCache(serverID)
|
||||||
|
|
||||||
as.serverService.StartAccServerRuntime(server)
|
as.serverService.StartAccServerRuntime(server)
|
||||||
@@ -433,3 +510,11 @@ func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.C
|
|||||||
_, _, err = as.updateConfigFiles(context.Background(), server, ConfigurationJson, &configMap, true)
|
_, _, err = as.updateConfigFiles(context.Background(), server, ConfigurationJson, &configMap, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *ConfigService) SetServerName(server *model.Server, name string) error {
|
||||||
|
configMap := map[string]interface{}{
|
||||||
|
"serverName": name,
|
||||||
|
}
|
||||||
|
_, _, err := as.updateConfigFiles(context.Background(), server, SettingsJson, &configMap, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,189 +17,21 @@ func NewLeaderboardService(repo *repository.LeaderboardRepository) *LeaderboardS
|
|||||||
return &LeaderboardService{repo: repo}
|
return &LeaderboardService{repo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the leaderboard for a server in the wire format (LeaderboardInput).
|
// Get returns the leaderboard for a server.
|
||||||
func (s *LeaderboardService) Get(ctx context.Context, serverID uuid.UUID) (*model.LeaderboardInput, error) {
|
func (s *LeaderboardService) Get(ctx context.Context, serverID uuid.UUID) (*model.Leaderboard, error) {
|
||||||
lb, err := s.repo.GetOrCreateByServerID(ctx, serverID)
|
return s.repo.GetOrCreateByServerID(ctx, serverID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return toInput(lb), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update replaces the leaderboard for a server from wire format input.
|
// Update replaces the leaderboard for a server.
|
||||||
func (s *LeaderboardService) Update(ctx context.Context, serverID uuid.UUID, input *model.LeaderboardInput) (*model.LeaderboardInput, error) {
|
func (s *LeaderboardService) Update(ctx context.Context, serverID uuid.UUID, lb *model.Leaderboard) (*model.Leaderboard, error) {
|
||||||
lb, err := fromInput(serverID, input)
|
if lb == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result, err := s.repo.FullReplace(ctx, serverID, lb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return toInput(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toInput converts the normalized DB model to the flat wire format.
|
|
||||||
func toInput(lb *model.Leaderboard) *model.LeaderboardInput {
|
|
||||||
// Build driver map (id -> index in slice) for result ordering
|
|
||||||
driverIndexByID := make(map[uuid.UUID]int, len(lb.Drivers))
|
|
||||||
|
|
||||||
// Sort drivers by position
|
|
||||||
drivers := make([]model.LeaderboardDriverInput, len(lb.Drivers))
|
|
||||||
for i, d := range lb.Drivers {
|
|
||||||
drivers[i] = model.LeaderboardDriverInput{
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Initials: d.Initials,
|
|
||||||
}
|
|
||||||
driverIndexByID[d.ID] = d.Position
|
|
||||||
}
|
|
||||||
// Re-index by position order
|
|
||||||
driverByPos := make(map[int]model.LeaderboardDriver, len(lb.Drivers))
|
|
||||||
for _, d := range lb.Drivers {
|
|
||||||
driverByPos[d.Position] = d
|
|
||||||
}
|
|
||||||
driverCount := len(lb.Drivers)
|
|
||||||
orderedDrivers := make([]model.LeaderboardDriver, driverCount)
|
|
||||||
for i := 0; i < driverCount; i++ {
|
|
||||||
if d, ok := driverByPos[i]; ok {
|
|
||||||
orderedDrivers[i] = d
|
|
||||||
drivers[i] = model.LeaderboardDriverInput{
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Initials: d.Initials,
|
|
||||||
}
|
|
||||||
driverIndexByID[d.ID] = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build tracks
|
|
||||||
raceByPos := make(map[int]model.LeaderboardRace, len(lb.Races))
|
|
||||||
for _, r := range lb.Races {
|
|
||||||
raceByPos[r.Position] = r
|
|
||||||
}
|
|
||||||
tracks := make([]model.LeaderboardTrackInput, len(lb.Races))
|
|
||||||
for i := 0; i < len(lb.Races); i++ {
|
|
||||||
race, ok := raceByPos[i]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results := make([]interface{}, driverCount)
|
|
||||||
for j := range results {
|
|
||||||
results[j] = "0"
|
|
||||||
}
|
|
||||||
for _, res := range race.Results {
|
|
||||||
idx, found := driverIndexByID[res.DriverID]
|
|
||||||
if found && idx < driverCount {
|
|
||||||
results[idx] = res.Score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracks[i] = model.LeaderboardTrackInput{
|
|
||||||
Name: race.Name,
|
|
||||||
Results: results,
|
|
||||||
FastestLapInitials: race.FastestLapInitials,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build points table
|
|
||||||
pointsTable := make([]model.LeaderboardPointRowInput, len(lb.PointRows))
|
|
||||||
for i, p := range lb.PointRows {
|
|
||||||
pointsTable[i] = model.LeaderboardPointRowInput{
|
|
||||||
Points: p.Points,
|
|
||||||
Label: p.Label,
|
|
||||||
Color: p.Color,
|
|
||||||
TextColor: p.TextColor,
|
|
||||||
Priority: p.Priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flColor := lb.FLColor
|
|
||||||
if flColor == "" {
|
|
||||||
flColor = "#8b5cf6"
|
|
||||||
}
|
|
||||||
flTextColor := lb.FLTextColor
|
|
||||||
if flTextColor == "" {
|
|
||||||
flTextColor = "#000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.LeaderboardInput{
|
|
||||||
Drivers: drivers,
|
|
||||||
PointsTable: pointsTable,
|
|
||||||
FLPoints: model.LeaderboardFLInput{
|
|
||||||
Points: lb.FLPoints,
|
|
||||||
Label: "FL +1",
|
|
||||||
Color: flColor,
|
|
||||||
TextColor: flTextColor,
|
|
||||||
Priority: len(pointsTable) + 1,
|
|
||||||
},
|
|
||||||
Tracks: tracks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fromInput converts the wire format to the normalized DB model.
|
|
||||||
func fromInput(serverID uuid.UUID, input *model.LeaderboardInput) (*model.Leaderboard, error) {
|
|
||||||
if input == nil {
|
|
||||||
return nil, fmt.Errorf("input is required")
|
return nil, fmt.Errorf("input is required")
|
||||||
}
|
}
|
||||||
|
for i := range lb.Drivers {
|
||||||
lb := &model.Leaderboard{
|
lb.Drivers[i].Position = i
|
||||||
ServerID: serverID,
|
|
||||||
FLPoints: input.FLPoints.Points,
|
|
||||||
FLColor: input.FLPoints.Color,
|
|
||||||
FLTextColor: input.FLPoints.TextColor,
|
|
||||||
}
|
}
|
||||||
|
for i := range lb.Races {
|
||||||
// Build drivers
|
lb.Races[i].Position = i
|
||||||
drivers := make([]model.LeaderboardDriver, len(input.Drivers))
|
|
||||||
driverIDs := make([]uuid.UUID, len(input.Drivers))
|
|
||||||
for i, d := range input.Drivers {
|
|
||||||
id := uuid.New()
|
|
||||||
driverIDs[i] = id
|
|
||||||
drivers[i] = model.LeaderboardDriver{
|
|
||||||
ID: id,
|
|
||||||
Name: d.Name,
|
|
||||||
Initials: d.Initials,
|
|
||||||
Color: d.Color,
|
|
||||||
Position: i,
|
|
||||||
}
|
}
|
||||||
}
|
return s.repo.FullReplace(ctx, serverID, lb)
|
||||||
lb.Drivers = drivers
|
|
||||||
|
|
||||||
// Build point rows
|
|
||||||
pointRows := make([]model.LeaderboardPointRow, len(input.PointsTable))
|
|
||||||
for i, p := range input.PointsTable {
|
|
||||||
pointRows[i] = model.LeaderboardPointRow{
|
|
||||||
Label: p.Label,
|
|
||||||
Points: p.Points,
|
|
||||||
Color: p.Color,
|
|
||||||
TextColor: p.TextColor,
|
|
||||||
Priority: p.Priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lb.PointRows = pointRows
|
|
||||||
|
|
||||||
// Build races
|
|
||||||
races := make([]model.LeaderboardRace, len(input.Tracks))
|
|
||||||
for i, t := range input.Tracks {
|
|
||||||
results := make([]model.LeaderboardResult, 0, len(t.Results))
|
|
||||||
for j, score := range t.Results {
|
|
||||||
if j >= len(driverIDs) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
scoreStr := fmt.Sprintf("%v", score)
|
|
||||||
results = append(results, model.LeaderboardResult{
|
|
||||||
DriverID: driverIDs[j],
|
|
||||||
Score: scoreStr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
races[i] = model.LeaderboardRace{
|
|
||||||
Name: t.Name,
|
|
||||||
Position: i,
|
|
||||||
FastestLapInitials: t.FastestLapInitials,
|
|
||||||
Results: results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lb.Races = races
|
|
||||||
|
|
||||||
return lb, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,6 +388,10 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
|||||||
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.configService.SetServerName(server, server.Name); err != nil {
|
||||||
|
logging.Error("Failed to set server name in config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), nil
|
return fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "0.10.7"
|
Version = "0.12.3"
|
||||||
Prefix = "v1"
|
Prefix = "v1"
|
||||||
Secret string
|
Secret string
|
||||||
SecretCode string
|
SecretCode string
|
||||||
|
|||||||
2
local/utl/server/server.go
Normal file → Executable file
2
local/utl/server/server.go
Normal file → Executable file
@@ -44,7 +44,7 @@ func Start(di *dig.Container) *fiber.App {
|
|||||||
|
|
||||||
app.Use(cors.New(cors.Config{
|
app.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: allowedOrigin,
|
AllowOrigins: allowedOrigin,
|
||||||
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
|
AllowHeaders: "Origin, Content-Type, Accept, Authorization, Access-Token",
|
||||||
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
|
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
MaxAge: 86400, // 24 hours
|
MaxAge: 86400, // 24 hours
|
||||||
|
|||||||
Reference in New Issue
Block a user