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("/", 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)
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
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"
|
"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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user