add statistics retrieve method
This commit is contained in:
@@ -26,6 +26,7 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com
|
||||
}
|
||||
|
||||
routeGroups.StateHistory.Get("/", ac.getAll)
|
||||
routeGroups.StateHistory.Get("/statistics", ac.getStatistics)
|
||||
|
||||
return ac
|
||||
}
|
||||
@@ -36,7 +37,7 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com
|
||||
// @Description Return StateHistorys
|
||||
// @Tags StateHistory
|
||||
// @Success 200 {array} string
|
||||
// @Router /v1/StateHistory [get]
|
||||
// @Router /v1/state-history [get]
|
||||
func (ac *StateHistoryController) getAll(c *fiber.Ctx) error {
|
||||
var filter model.StateHistoryFilter
|
||||
if err := common.ParseQueryFilter(c, &filter); err != nil {
|
||||
@@ -54,3 +55,28 @@ func (ac *StateHistoryController) getAll(c *fiber.Ctx) error {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -118,3 +118,144 @@ func (c *LookupCache) Clear() {
|
||||
|
||||
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])
|
||||
}
|
||||
@@ -53,7 +53,19 @@ type StateHistory struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ServerID uint `json:"serverId" gorm:"not null"`
|
||||
Session string `json:"session"`
|
||||
Track string `json:"track"`
|
||||
PlayerCount int `json:"playerCount"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
SessionStart time.Time `json:"sessionStart"`
|
||||
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"`
|
||||
}
|
||||
38
local/model/stateHistoryStats.go
Normal file
38
local/model/stateHistoryStats.go
Normal 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"`
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -64,12 +65,18 @@ type ConfigService struct {
|
||||
repository *repository.ConfigRepository
|
||||
serverRepository *repository.ServerRepository
|
||||
serverService *ServerService
|
||||
configCache *model.ServerConfigCache
|
||||
}
|
||||
|
||||
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService {
|
||||
return &ConfigService{
|
||||
repository: repository,
|
||||
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
|
||||
}
|
||||
|
||||
// Invalidate all configs for this server since configs can be interdependent
|
||||
as.configCache.InvalidateServerCache(strconv.Itoa(serverID))
|
||||
|
||||
as.serverService.StartAccServerRuntime(server)
|
||||
|
||||
// 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) {
|
||||
serverID, _ := ctx.ParamsInt("id")
|
||||
configFile := ctx.Params("file")
|
||||
serverIDStr := strconv.Itoa(serverID)
|
||||
|
||||
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
|
||||
|
||||
if err != nil {
|
||||
log.Print("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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// GetConfigs
|
||||
// Gets physical config file and caches it in database.
|
||||
//
|
||||
// Args:
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
// Gets all configurations for a server, using cache when possible.
|
||||
func (as ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) {
|
||||
serverID, _ := ctx.ParamsInt("id")
|
||||
serverIDStr := strconv.Itoa(serverID)
|
||||
|
||||
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
|
||||
|
||||
if err != nil {
|
||||
log.Print("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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
configs.Settings = settings
|
||||
as.configCache.UpdateSettings(serverIDStr, settings)
|
||||
}
|
||||
|
||||
return &model.Configurations{
|
||||
Configuration: decodedconfiguration,
|
||||
Event: decodedevent,
|
||||
EventRules: decodedeventRules,
|
||||
Settings: decodedsettings,
|
||||
AssistRules: decodedAssistRules,
|
||||
}, nil
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func readAndDecode[T interface{}](path string, configFile string) (T, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -23,7 +24,7 @@ type ServerService struct {
|
||||
lastInsertTimes sync.Map // Track last insert time per server
|
||||
debouncers sync.Map // Track debounce timers 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 {
|
||||
@@ -91,44 +92,96 @@ func (s *ServerService) shouldInsertStateHistory(serverID uint) bool {
|
||||
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) {
|
||||
// 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{
|
||||
ServerID: serverID,
|
||||
Session: state.Session,
|
||||
Track: state.Track,
|
||||
PlayerCount: state.PlayerCount,
|
||||
DateCreated: time.Now().UTC(),
|
||||
SessionStart: state.SessionStart,
|
||||
SessionDurationMinutes: state.SessionDurationMinutes,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) {
|
||||
sessionsInterface, exists := s.sessionCache.Load(server.ID)
|
||||
if !exists {
|
||||
// Try to load sessions from config
|
||||
event, err := DecodeFileName(EventJson)(server.ConfigPath)
|
||||
serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
|
||||
|
||||
// Get event config from cache or load it
|
||||
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 {
|
||||
logging.Error("Failed to load event config for server %d: %v", server.ID, err)
|
||||
return
|
||||
}
|
||||
evt := event.(model.EventConfig)
|
||||
s.sessionCache.Store(server.ID, evt.Sessions)
|
||||
sessionsInterface = evt.Sessions
|
||||
s.configService.configCache.UpdateEvent(serverIDStr, event)
|
||||
}
|
||||
|
||||
sessions := sessionsInterface.([]model.Session)
|
||||
if sessionType == "" && len(sessions) > 0 {
|
||||
sessionType = sessions[0].SessionType
|
||||
var configuration model.Configuration
|
||||
if cached, ok := s.configService.configCache.GetConfiguration(serverIDStr); ok {
|
||||
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 {
|
||||
if session.SessionType == sessionType {
|
||||
s.configService.configCache.UpdateConfiguration(serverIDStr, configuration)
|
||||
}
|
||||
|
||||
if instance, ok := s.instances.Load(server.ID); ok {
|
||||
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.Session = sessionType
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
||||
@@ -177,16 +230,9 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
|
||||
instance = instanceInterface.(*tracking.AccServerInstance)
|
||||
}
|
||||
|
||||
config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath)
|
||||
cfg := config.(model.Configuration)
|
||||
event, _ := DecodeFileName(EventJson)(server.ConfigPath)
|
||||
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)
|
||||
// Invalidate config cache for this server before loading new configs
|
||||
serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
|
||||
s.configService.configCache.InvalidateServerCache(serverIDStr)
|
||||
|
||||
s.updateSessionDuration(server, instance.State.Session)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/repository"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@@ -41,3 +43,151 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory)
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user