add statistics retrieve method

This commit is contained in:
Fran Jurmanović
2025-05-30 13:31:28 +02:00
parent 26c6bc5496
commit 9c9e28350f
7 changed files with 555 additions and 58 deletions

View File

@@ -26,6 +26,7 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com
} }
routeGroups.StateHistory.Get("/", ac.getAll) routeGroups.StateHistory.Get("/", ac.getAll)
routeGroups.StateHistory.Get("/statistics", ac.getStatistics)
return ac return ac
} }
@@ -36,7 +37,7 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com
// @Description Return StateHistorys // @Description Return StateHistorys
// @Tags StateHistory // @Tags StateHistory
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/StateHistory [get] // @Router /v1/state-history [get]
func (ac *StateHistoryController) getAll(c *fiber.Ctx) error { func (ac *StateHistoryController) getAll(c *fiber.Ctx) error {
var filter model.StateHistoryFilter var filter model.StateHistoryFilter
if err := common.ParseQueryFilter(c, &filter); err != nil { if err := common.ParseQueryFilter(c, &filter); err != nil {
@@ -54,3 +55,28 @@ func (ac *StateHistoryController) getAll(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// getStatistics returns StateHistorys
//
// @Summary Return StateHistorys
// @Description Return StateHistorys
// @Tags StateHistory
// @Success 200 {array} string
// @Router /v1/state-history/statistics [get]
func (ac *StateHistoryController) getStatistics(c *fiber.Ctx) error {
var filter model.StateHistoryFilter
if err := common.ParseQueryFilter(c, &filter); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
result, err := ac.service.GetStatistics(c, &filter)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error retrieving state history statistics",
})
}
return c.JSON(result)
}

View File

@@ -118,3 +118,144 @@ func (c *LookupCache) Clear() {
c.data = make(map[string]interface{}) c.data = make(map[string]interface{})
} }
// ConfigEntry represents a cached configuration entry with its update time
type ConfigEntry[T any] struct {
Data T
UpdatedAt time.Time
}
// getConfigFromCache is a generic helper function to retrieve cached configs
func getConfigFromCache[T any](cache map[string]*ConfigEntry[T], serverID string, expirationTime time.Duration) (*T, bool) {
if entry, ok := cache[serverID]; ok {
if time.Since(entry.UpdatedAt) < expirationTime {
return &entry.Data, true
}
}
return nil, false
}
// updateConfigInCache is a generic helper function to update cached configs
func updateConfigInCache[T any](cache map[string]*ConfigEntry[T], serverID string, data T) {
cache[serverID] = &ConfigEntry[T]{
Data: data,
UpdatedAt: time.Now(),
}
}
// ServerConfigCache manages cached server configurations
type ServerConfigCache struct {
sync.RWMutex
configuration map[string]*ConfigEntry[Configuration]
assistRules map[string]*ConfigEntry[AssistRules]
event map[string]*ConfigEntry[EventConfig]
eventRules map[string]*ConfigEntry[EventRules]
settings map[string]*ConfigEntry[ServerSettings]
config CacheConfig
}
// NewServerConfigCache creates a new server configuration cache
func NewServerConfigCache(config CacheConfig) *ServerConfigCache {
return &ServerConfigCache{
configuration: make(map[string]*ConfigEntry[Configuration]),
assistRules: make(map[string]*ConfigEntry[AssistRules]),
event: make(map[string]*ConfigEntry[EventConfig]),
eventRules: make(map[string]*ConfigEntry[EventRules]),
settings: make(map[string]*ConfigEntry[ServerSettings]),
config: config,
}
}
// GetConfiguration retrieves a cached configuration
func (c *ServerConfigCache) GetConfiguration(serverID string) (*Configuration, bool) {
c.RLock()
defer c.RUnlock()
return getConfigFromCache(c.configuration, serverID, c.config.ExpirationTime)
}
// GetAssistRules retrieves cached assist rules
func (c *ServerConfigCache) GetAssistRules(serverID string) (*AssistRules, bool) {
c.RLock()
defer c.RUnlock()
return getConfigFromCache(c.assistRules, serverID, c.config.ExpirationTime)
}
// GetEvent retrieves cached event configuration
func (c *ServerConfigCache) GetEvent(serverID string) (*EventConfig, bool) {
c.RLock()
defer c.RUnlock()
return getConfigFromCache(c.event, serverID, c.config.ExpirationTime)
}
// GetEventRules retrieves cached event rules
func (c *ServerConfigCache) GetEventRules(serverID string) (*EventRules, bool) {
c.RLock()
defer c.RUnlock()
return getConfigFromCache(c.eventRules, serverID, c.config.ExpirationTime)
}
// GetSettings retrieves cached server settings
func (c *ServerConfigCache) GetSettings(serverID string) (*ServerSettings, bool) {
c.RLock()
defer c.RUnlock()
return getConfigFromCache(c.settings, serverID, c.config.ExpirationTime)
}
// UpdateConfiguration updates the configuration cache
func (c *ServerConfigCache) UpdateConfiguration(serverID string, config Configuration) {
c.Lock()
defer c.Unlock()
updateConfigInCache(c.configuration, serverID, config)
}
// UpdateAssistRules updates the assist rules cache
func (c *ServerConfigCache) UpdateAssistRules(serverID string, rules AssistRules) {
c.Lock()
defer c.Unlock()
updateConfigInCache(c.assistRules, serverID, rules)
}
// UpdateEvent updates the event configuration cache
func (c *ServerConfigCache) UpdateEvent(serverID string, event EventConfig) {
c.Lock()
defer c.Unlock()
updateConfigInCache(c.event, serverID, event)
}
// UpdateEventRules updates the event rules cache
func (c *ServerConfigCache) UpdateEventRules(serverID string, rules EventRules) {
c.Lock()
defer c.Unlock()
updateConfigInCache(c.eventRules, serverID, rules)
}
// UpdateSettings updates the server settings cache
func (c *ServerConfigCache) UpdateSettings(serverID string, settings ServerSettings) {
c.Lock()
defer c.Unlock()
updateConfigInCache(c.settings, serverID, settings)
}
// InvalidateServerCache removes all cached configurations for a specific server
func (c *ServerConfigCache) InvalidateServerCache(serverID string) {
c.Lock()
defer c.Unlock()
delete(c.configuration, serverID)
delete(c.assistRules, serverID)
delete(c.event, serverID)
delete(c.eventRules, serverID)
delete(c.settings, serverID)
}
// Clear removes all entries from the cache
func (c *ServerConfigCache) Clear() {
c.Lock()
defer c.Unlock()
c.configuration = make(map[string]*ConfigEntry[Configuration])
c.assistRules = make(map[string]*ConfigEntry[AssistRules])
c.event = make(map[string]*ConfigEntry[EventConfig])
c.eventRules = make(map[string]*ConfigEntry[EventRules])
c.settings = make(map[string]*ConfigEntry[ServerSettings])
}

View File

@@ -53,7 +53,19 @@ type StateHistory struct {
ID uint `gorm:"primaryKey" json:"id"` ID uint `gorm:"primaryKey" json:"id"`
ServerID uint `json:"serverId" gorm:"not null"` ServerID uint `json:"serverId" gorm:"not null"`
Session string `json:"session"` Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"` PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"` DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"` SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uint `json:"sessionId" gorm:"not null"` // Unique identifier for each session/event
}
type RecentSession struct {
ID uint `json:"id"`
Date time.Time `json:"date"`
Type string `json:"type"`
Track string `json:"track"`
Duration int `json:"duration"`
Players int `json:"players"`
} }

View File

@@ -0,0 +1,38 @@
package model
import "time"
type SessionCount struct {
Name string `json:"name"`
Count int `json:"count"`
}
type DailyActivity struct {
Date time.Time `json:"date"`
SessionsCount int `json:"sessionsCount"`
}
type PlayerCountPoint struct {
Timestamp time.Time `json:"timestamp"`
Count int `json:"count"`
}
type StateHistoryStats struct {
AveragePlayers float64 `json:"averagePlayers"`
PeakPlayers int `json:"peakPlayers"`
TotalSessions int `json:"totalSessions"`
TotalPlaytime int `json:"totalPlaytime"` // in minutes
PlayerCountOverTime []PlayerCountPoint `json:"playerCountOverTime"`
SessionTypes []SessionCount `json:"sessionTypes"`
DailyActivity []DailyActivity `json:"dailyActivity"`
RecentSessions []RecentSession `json:"recentSessions"`
}
type RecentSession struct {
ID uint `json:"id"`
Date time.Time `json:"date"`
Type string `json:"type"`
Track string `json:"track"`
Duration int `json:"duration"`
Players int `json:"players"`
}

View File

@@ -11,6 +11,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -64,12 +65,18 @@ type ConfigService struct {
repository *repository.ConfigRepository repository *repository.ConfigRepository
serverRepository *repository.ServerRepository serverRepository *repository.ServerRepository
serverService *ServerService serverService *ServerService
configCache *model.ServerConfigCache
} }
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService { func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService {
return &ConfigService{ return &ConfigService{
repository: repository, repository: repository,
serverRepository: serverRepository, serverRepository: serverRepository,
configCache: model.NewServerConfigCache(model.CacheConfig{
ExpirationTime: 5 * time.Minute, // Cache configs for 5 minutes
ThrottleTime: 1 * time.Second, // Prevent rapid re-reads
DefaultStatus: model.StatusUnknown,
}),
} }
} }
@@ -148,6 +155,9 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{
return nil, err return nil, err
} }
// Invalidate all configs for this server since configs can be interdependent
as.configCache.InvalidateServerCache(strconv.Itoa(serverID))
as.serverService.StartAccServerRuntime(server) as.serverService.StartAccServerRuntime(server)
// Log change // Log change
@@ -170,71 +180,145 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{
func (as ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) { func (as ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
serverID, _ := ctx.ParamsInt("id") serverID, _ := ctx.ParamsInt("id")
configFile := ctx.Params("file") configFile := ctx.Params("file")
serverIDStr := strconv.Itoa(serverID)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil { if err != nil {
log.Print("Server not found") log.Print("Server not found")
return nil, fiber.NewError(404, "Server not found") return nil, fiber.NewError(404, "Server not found")
} }
// Try to get from cache based on config file type
switch configFile {
case ConfigurationJson:
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
return cached, nil
}
case AssistRulesJson:
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
return cached, nil
}
case EventJson:
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
return cached, nil
}
case EventRulesJson:
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
return cached, nil
}
case SettingsJson:
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
return cached, nil
}
}
decoded, err := DecodeFileName(configFile)(server.ConfigPath) decoded, err := DecodeFileName(configFile)(server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Cache the result based on config file type
switch configFile {
case ConfigurationJson:
if config, ok := decoded.(model.Configuration); ok {
as.configCache.UpdateConfiguration(serverIDStr, config)
}
case AssistRulesJson:
if rules, ok := decoded.(model.AssistRules); ok {
as.configCache.UpdateAssistRules(serverIDStr, rules)
}
case EventJson:
if event, ok := decoded.(model.EventConfig); ok {
as.configCache.UpdateEvent(serverIDStr, event)
}
case EventRulesJson:
if rules, ok := decoded.(model.EventRules); ok {
as.configCache.UpdateEventRules(serverIDStr, rules)
}
case SettingsJson:
if settings, ok := decoded.(model.ServerSettings); ok {
as.configCache.UpdateSettings(serverIDStr, settings)
}
}
return decoded, nil return decoded, nil
} }
// GetConfigs // GetConfigs
// Gets physical config file and caches it in database. // Gets all configurations for a server, using cache when possible.
//
// Args:
// context.Context: Application context
// Returns:
// string: Application version
func (as ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) { func (as ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) {
serverID, _ := ctx.ParamsInt("id") serverID, _ := ctx.ParamsInt("id")
serverIDStr := strconv.Itoa(serverID)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil { if err != nil {
log.Print("Server not found") log.Print("Server not found")
return nil, fiber.NewError(404, "Server not found") return nil, fiber.NewError(404, "Server not found")
} }
decodedconfiguration, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath) configs := &model.Configurations{}
// Load configuration
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
configs.Configuration = *cached
} else {
config, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs.Configuration = config
as.configCache.UpdateConfiguration(serverIDStr, config)
}
decodedAssistRules, err := mustDecode[model.AssistRules](AssistRulesJson, server.ConfigPath) // Load assist rules
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
configs.AssistRules = *cached
} else {
rules, err := mustDecode[model.AssistRules](AssistRulesJson, server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs.AssistRules = rules
as.configCache.UpdateAssistRules(serverIDStr, rules)
}
decodedevent, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath) // Load event config
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
configs.Event = *cached
} else {
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs.Event = event
as.configCache.UpdateEvent(serverIDStr, event)
}
decodedeventRules, err := mustDecode[model.EventRules](EventRulesJson, server.ConfigPath) // Load event rules
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
configs.EventRules = *cached
} else {
rules, err := mustDecode[model.EventRules](EventRulesJson, server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs.EventRules = rules
as.configCache.UpdateEventRules(serverIDStr, rules)
}
decodedsettings, err := mustDecode[model.ServerSettings](SettingsJson, server.ConfigPath) // Load settings
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
configs.Settings = *cached
} else {
settings, err := mustDecode[model.ServerSettings](SettingsJson, server.ConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs.Settings = settings
as.configCache.UpdateSettings(serverIDStr, settings)
}
return &model.Configurations{ return configs, nil
Configuration: decodedconfiguration,
Event: decodedevent,
EventRules: decodedeventRules,
Settings: decodedsettings,
AssistRules: decodedAssistRules,
}, nil
} }
func readAndDecode[T interface{}](path string, configFile string) (T, error) { func readAndDecode[T interface{}](path string, configFile string) (T, error) {

View File

@@ -8,6 +8,7 @@ import (
"context" "context"
"log" "log"
"path/filepath" "path/filepath"
"strconv"
"sync" "sync"
"time" "time"
@@ -23,7 +24,7 @@ type ServerService struct {
lastInsertTimes sync.Map // Track last insert time per server lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server logTailers sync.Map // Track log tailers per server
sessionCache sync.Map // Cache of server event sessions sessionIDs sync.Map // Track current session ID per server
} }
type pendingState struct { type pendingState struct {
@@ -91,45 +92,97 @@ func (s *ServerService) shouldInsertStateHistory(serverID uint) bool {
return false return false
} }
func (s *ServerService) getNextSessionID(serverID uint) uint {
currentID, _ := s.sessionIDs.LoadOrStore(serverID, uint(0))
nextID := currentID.(uint) + 1
s.sessionIDs.Store(serverID, nextID)
return nextID
}
func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerState) { func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerState) {
// Get or create session ID when session changes
currentSessionInterface, exists := s.instances.Load(serverID)
var sessionID uint
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
serverInstance := currentSessionInterface.(*tracking.AccServerInstance)
if serverInstance.State == nil || serverInstance.State.Session != state.Session {
sessionID = s.getNextSessionID(serverID)
} else {
sessionIDInterface, exists := s.sessionIDs.Load(serverID)
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
sessionID = sessionIDInterface.(uint)
}
}
}
s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{ s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{
ServerID: serverID, ServerID: serverID,
Session: state.Session, Session: state.Session,
Track: state.Track,
PlayerCount: state.PlayerCount, PlayerCount: state.PlayerCount,
DateCreated: time.Now().UTC(), DateCreated: time.Now().UTC(),
SessionStart: state.SessionStart,
SessionDurationMinutes: state.SessionDurationMinutes, SessionDurationMinutes: state.SessionDurationMinutes,
SessionID: sessionID,
}) })
} }
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) { func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) {
sessionsInterface, exists := s.sessionCache.Load(server.ID) serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
if !exists {
// Try to load sessions from config // Get event config from cache or load it
event, err := DecodeFileName(EventJson)(server.ConfigPath) var event model.EventConfig
if cached, ok := s.configService.configCache.GetEvent(serverIDStr); ok {
event = *cached
} else {
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
if err != nil { if err != nil {
logging.Error("Failed to load event config for server %d: %v", server.ID, err) logging.Error("Failed to load event config for server %d: %v", server.ID, err)
return return
} }
evt := event.(model.EventConfig) s.configService.configCache.UpdateEvent(serverIDStr, event)
s.sessionCache.Store(server.ID, evt.Sessions)
sessionsInterface = evt.Sessions
} }
sessions := sessionsInterface.([]model.Session) var configuration model.Configuration
if sessionType == "" && len(sessions) > 0 { if cached, ok := s.configService.configCache.GetConfiguration(serverIDStr); ok {
sessionType = sessions[0].SessionType configuration = *cached
} else {
configuration, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
if err != nil {
logging.Error("Failed to load configuration config for server %d: %v", server.ID, err)
return
} }
for _, session := range sessions { s.configService.configCache.UpdateConfiguration(serverIDStr, configuration)
if session.SessionType == sessionType { }
if instance, ok := s.instances.Load(server.ID); ok { if instance, ok := s.instances.Load(server.ID); ok {
serverInstance := instance.(*tracking.AccServerInstance) serverInstance := instance.(*tracking.AccServerInstance)
serverInstance.State.Track = event.Track
serverInstance.State.MaxConnections = configuration.MaxConnections.ToInt()
// Check if session type has changed
if serverInstance.State.Session != sessionType {
// Get new session ID for the new session
sessionID := s.getNextSessionID(server.ID)
s.sessionIDs.Store(server.ID, sessionID)
}
if sessionType == "" && len(event.Sessions) > 0 {
sessionType = event.Sessions[0].SessionType
}
for _, session := range event.Sessions {
if session.SessionType == sessionType {
serverInstance.State.SessionDurationMinutes = session.SessionDurationMinutes.ToInt() serverInstance.State.SessionDurationMinutes = session.SessionDurationMinutes.ToInt()
serverInstance.State.Session = sessionType serverInstance.State.Session = sessionType
}
break break
} }
} }
} }
}
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) { func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
// Update session duration when session changes // Update session duration when session changes
@@ -177,16 +230,9 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
instance = instanceInterface.(*tracking.AccServerInstance) instance = instanceInterface.(*tracking.AccServerInstance)
} }
config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath) // Invalidate config cache for this server before loading new configs
cfg := config.(model.Configuration) serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
event, _ := DecodeFileName(EventJson)(server.ConfigPath) s.configService.configCache.InvalidateServerCache(serverIDStr)
evt := event.(model.EventConfig)
instance.State.MaxConnections = cfg.MaxConnections.ToInt()
instance.State.Track = evt.Track
// Cache sessions for duration lookup
s.sessionCache.Store(server.ID, evt.Sessions)
s.updateSessionDuration(server, instance.State.Session) s.updateSessionDuration(server, instance.State.Session)

View File

@@ -4,6 +4,8 @@ import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"log" "log"
"sort"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -41,3 +43,151 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory)
} }
return nil return nil
} }
func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*model.StateHistoryStats, error) {
// Get all state history entries based on filter
entries, err := s.repository.GetAll(ctx.UserContext(), filter)
if err != nil {
log.Printf("Error getting state history for statistics: %v", err)
return nil, err
}
stats := &model.StateHistoryStats{
PlayerCountOverTime: make([]model.PlayerCountPoint, 0),
SessionTypes: make([]model.SessionCount, 0),
DailyActivity: make([]model.DailyActivity, 0),
RecentSessions: make([]model.RecentSession, 0),
}
if len(*entries) == 0 {
return stats, nil
}
// Maps to track unique sessions and their details
sessionMap := make(map[uint]*struct {
StartTime time.Time
EndTime time.Time
Session string
Track string
MaxPlayers int
})
// Maps for aggregating statistics
dailySessionCount := make(map[string]int)
sessionTypeCount := make(map[string]int)
totalPlayers := 0
peakPlayers := 0
// Process each state history entry
for _, entry := range *entries {
// Track player count over time
stats.PlayerCountOverTime = append(stats.PlayerCountOverTime, model.PlayerCountPoint{
Timestamp: entry.DateCreated,
Count: entry.PlayerCount,
})
// Update peak players
if entry.PlayerCount > peakPlayers {
peakPlayers = entry.PlayerCount
}
totalPlayers += entry.PlayerCount
// Process session information using SessionID
if _, exists := sessionMap[entry.SessionID]; !exists {
sessionMap[entry.SessionID] = &struct {
StartTime time.Time
EndTime time.Time
Session string
Track string
MaxPlayers int
}{
StartTime: entry.DateCreated,
Session: entry.Session,
Track: entry.Track,
MaxPlayers: entry.PlayerCount,
}
// Count session types
sessionTypeCount[entry.Session]++
// Count daily sessions
dateStr := entry.DateCreated.Format("2006-01-02")
dailySessionCount[dateStr]++
} else {
session := sessionMap[entry.SessionID]
session.EndTime = entry.DateCreated
if entry.PlayerCount > session.MaxPlayers {
session.MaxPlayers = entry.PlayerCount
}
}
}
// Calculate statistics
stats.PeakPlayers = peakPlayers
stats.TotalSessions = len(sessionMap)
if len(*entries) > 0 {
stats.AveragePlayers = float64(totalPlayers) / float64(len(*entries))
}
// Process session types
for sessionType, count := range sessionTypeCount {
stats.SessionTypes = append(stats.SessionTypes, model.SessionCount{
Name: sessionType,
Count: count,
})
}
// Process daily activity
for dateStr, count := range dailySessionCount {
date, _ := time.Parse("2006-01-02", dateStr)
stats.DailyActivity = append(stats.DailyActivity, model.DailyActivity{
Date: date,
SessionsCount: count,
})
}
// Calculate total playtime and prepare recent sessions
var recentSessions []model.RecentSession
totalPlaytime := 0
for sessionID, session := range sessionMap {
if !session.EndTime.IsZero() {
duration := int(session.EndTime.Sub(session.StartTime).Minutes())
totalPlaytime += duration
recentSessions = append(recentSessions, model.RecentSession{
ID: sessionID,
Date: session.StartTime,
Type: session.Session,
Track: session.Track,
Duration: duration,
Players: session.MaxPlayers,
})
}
}
stats.TotalPlaytime = totalPlaytime
// Sort recent sessions by date (newest first) and limit to last 10
sort.Slice(recentSessions, func(i, j int) bool {
return recentSessions[i].Date.After(recentSessions[j].Date)
})
if len(recentSessions) > 10 {
recentSessions = recentSessions[:10]
}
stats.RecentSessions = recentSessions
// Sort daily activity by date
sort.Slice(stats.DailyActivity, func(i, j int) bool {
return stats.DailyActivity[i].Date.Before(stats.DailyActivity[j].Date)
})
// Sort player count over time by timestamp
sort.Slice(stats.PlayerCountOverTime, func(i, j int) bool {
return stats.PlayerCountOverTime[i].Timestamp.Before(stats.PlayerCountOverTime[j].Timestamp)
})
return stats, nil
}