Add runtime mock toggles and auth-safe dev assets

This commit is contained in:
rcourtman
2025-09-30 10:02:26 +00:00
parent eb82745638
commit 6676b7f722
6 changed files with 460 additions and 126 deletions
+29
View File
@@ -2271,6 +2271,35 @@ func (m *Manager) CleanupAlertsForNodes(existingNodes map[string]bool) {
}
}
// ClearActiveAlerts removes all active and pending alerts, resetting the manager state.
func (m *Manager) ClearActiveAlerts() {
m.mu.Lock()
if len(m.activeAlerts) == 0 && len(m.pendingAlerts) == 0 {
m.mu.Unlock()
return
}
m.activeAlerts = make(map[string]*Alert)
m.pendingAlerts = make(map[string]time.Time)
m.recentAlerts = make(map[string]*Alert)
m.suppressedUntil = make(map[string]time.Time)
m.alertRateLimit = make(map[string][]time.Time)
m.nodeOfflineCount = make(map[string]int)
m.offlineConfirmations = make(map[string]int)
m.mu.Unlock()
m.resolvedMutex.Lock()
m.recentlyResolved = make(map[string]*ResolvedAlert)
m.resolvedMutex.Unlock()
log.Info().Msg("Cleared all active and pending alerts")
go func() {
if err := m.SaveActiveAlerts(); err != nil {
log.Error().Err(err).Msg("Failed to persist cleared alerts")
}
}()
}
// periodicSaveAlerts saves active alerts to disk periodically
func (m *Manager) periodicSaveAlerts() {
ticker := time.NewTicker(1 * time.Minute)
+102 -4
View File
@@ -18,6 +18,7 @@ import (
internalauth "github.com/rcourtman/pulse-go-rewrite/internal/auth"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
@@ -620,7 +621,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
// HandleGetNodes returns all configured nodes
func (h *ConfigHandlers) HandleGetNodes(w http.ResponseWriter, r *http.Request) {
// Check if mock mode is enabled
if os.Getenv("PULSE_MOCK_MODE") == "true" {
if mock.IsMockEnabled() {
// Return mock nodes for settings page
mockNodes := []NodeResponse{}
@@ -788,7 +789,7 @@ func extractHostAndPort(hostStr string) (string, string, error) {
// HandleAddNode adds a new node
func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
// Prevent node modifications in mock mode
if os.Getenv("PULSE_MOCK_MODE") == "true" {
if mock.IsMockEnabled() {
http.Error(w, "Cannot modify nodes in mock mode. Please disable mock mode first: /opt/pulse/scripts/toggle-mock.sh off", http.StatusForbidden)
return
}
@@ -1270,7 +1271,7 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
// HandleUpdateNode updates an existing node
func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request) {
// Prevent node modifications in mock mode
if os.Getenv("PULSE_MOCK_MODE") == "true" {
if mock.IsMockEnabled() {
http.Error(w, "Cannot modify nodes in mock mode", http.StatusForbidden)
return
}
@@ -1511,7 +1512,7 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
// HandleDeleteNode deletes a node
func (h *ConfigHandlers) HandleDeleteNode(w http.ResponseWriter, r *http.Request) {
// Prevent node modifications in mock mode
if os.Getenv("PULSE_MOCK_MODE") == "true" {
if mock.IsMockEnabled() {
http.Error(w, "Cannot modify nodes in mock mode", http.StatusForbidden)
return
}
@@ -3182,6 +3183,103 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req
json.NewEncoder(w).Encode(response)
}
// HandleGetMockMode returns the current mock mode state and configuration.
func (h *ConfigHandlers) HandleGetMockMode(w http.ResponseWriter, r *http.Request) {
status := struct {
Enabled bool `json:"enabled"`
Config mock.MockConfig `json:"config"`
}{
Enabled: mock.IsMockEnabled(),
Config: mock.GetConfig(),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(status); err != nil {
log.Error().Err(err).Msg("Failed to encode mock mode status")
}
}
type mockModeRequest struct {
Enabled *bool `json:"enabled"`
Config struct {
NodeCount *int `json:"nodeCount"`
VMsPerNode *int `json:"vmsPerNode"`
LXCsPerNode *int `json:"lxcsPerNode"`
RandomMetrics *bool `json:"randomMetrics"`
HighLoadNodes []string `json:"highLoadNodes"`
StoppedPercent *float64 `json:"stoppedPercent"`
} `json:"config"`
}
// HandleUpdateMockMode updates mock mode and optionally its configuration.
func (h *ConfigHandlers) HandleUpdateMockMode(w http.ResponseWriter, r *http.Request) {
var req mockModeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Error().Err(err).Msg("Failed to decode mock mode request")
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Update configuration first if provided.
currentCfg := mock.GetConfig()
if req.Config.NodeCount != nil {
if *req.Config.NodeCount <= 0 {
http.Error(w, "nodeCount must be greater than zero", http.StatusBadRequest)
return
}
currentCfg.NodeCount = *req.Config.NodeCount
}
if req.Config.VMsPerNode != nil {
if *req.Config.VMsPerNode < 0 {
http.Error(w, "vmsPerNode cannot be negative", http.StatusBadRequest)
return
}
currentCfg.VMsPerNode = *req.Config.VMsPerNode
}
if req.Config.LXCsPerNode != nil {
if *req.Config.LXCsPerNode < 0 {
http.Error(w, "lxcsPerNode cannot be negative", http.StatusBadRequest)
return
}
currentCfg.LXCsPerNode = *req.Config.LXCsPerNode
}
if req.Config.RandomMetrics != nil {
currentCfg.RandomMetrics = *req.Config.RandomMetrics
}
if req.Config.HighLoadNodes != nil {
currentCfg.HighLoadNodes = req.Config.HighLoadNodes
}
if req.Config.StoppedPercent != nil {
if *req.Config.StoppedPercent < 0 || *req.Config.StoppedPercent > 1 {
http.Error(w, "stoppedPercent must be between 0 and 1", http.StatusBadRequest)
return
}
currentCfg.StoppedPercent = *req.Config.StoppedPercent
}
mock.SetMockConfig(currentCfg)
if req.Enabled != nil {
if h.monitor != nil {
h.monitor.SetMockMode(*req.Enabled)
} else {
mock.SetEnabled(*req.Enabled)
}
}
w.Header().Set("Content-Type", "application/json")
status := struct {
Enabled bool `json:"enabled"`
Config mock.MockConfig `json:"config"`
}{
Enabled: mock.IsMockEnabled(),
Config: mock.GetConfig(),
}
if err := json.NewEncoder(w).Encode(status); err != nil {
log.Error().Err(err).Msg("Failed to encode mock mode response")
}
}
// AutoRegisterRequest represents a request from the setup script to auto-register a node
type AutoRegisterRequest struct {
Type string `json:"type"` // "pve" or "pbs"
+21 -1
View File
@@ -184,6 +184,18 @@ func (r *Router) setupRoutes() {
}
})
// Mock mode toggle routes
r.mux.HandleFunc("/api/system/mock-mode", func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
configHandlers.HandleGetMockMode(w, req)
case http.MethodPost, http.MethodPut:
RequireAdmin(configHandlers.config, configHandlers.HandleUpdateMockMode)(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// Registration token routes removed - feature deprecated
// Security routes
@@ -884,13 +896,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Also allow static assets without auth (JS, CSS, etc)
// These MUST be accessible for the login page to work
isStaticAsset := strings.HasPrefix(req.URL.Path, "/assets/") ||
strings.HasPrefix(req.URL.Path, "/@vite/") ||
strings.HasPrefix(req.URL.Path, "/@solid-refresh") ||
strings.HasPrefix(req.URL.Path, "/src/") ||
strings.HasPrefix(req.URL.Path, "/node_modules/") ||
req.URL.Path == "/" ||
req.URL.Path == "/index.html" ||
req.URL.Path == "/favicon.ico" ||
req.URL.Path == "/logo.svg" ||
strings.HasSuffix(req.URL.Path, ".js") ||
strings.HasSuffix(req.URL.Path, ".css") ||
strings.HasSuffix(req.URL.Path, ".map")
strings.HasSuffix(req.URL.Path, ".map") ||
strings.HasSuffix(req.URL.Path, ".ts") ||
strings.HasSuffix(req.URL.Path, ".tsx") ||
strings.HasSuffix(req.URL.Path, ".mjs") ||
strings.HasSuffix(req.URL.Path, ".jsx")
isPublic := isStaticAsset
for _, path := range publicPaths {
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
"github.com/rs/zerolog/log"
)
@@ -446,7 +447,7 @@ type SystemSettings struct {
func (c *ConfigPersistence) SaveNodesConfig(pveInstances []PVEInstance, pbsInstances []PBSInstance) error {
// CRITICAL: Prevent saving empty nodes when in mock mode
// Mock mode should NEVER modify real node configuration
if os.Getenv("PULSE_MOCK_MODE") == "true" {
if mock.IsMockEnabled() {
log.Warn().Msg("Skipping nodes save - mock mode is enabled")
return nil // Silently succeed to prevent errors but don't save
}
+211 -98
View File
@@ -3,6 +3,8 @@ package mock
import (
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
@@ -10,63 +12,175 @@ import (
)
var (
mockData models.StateSnapshot
// Removed mockAlerts - using real alert manager instead
mockAlertHistory []models.Alert
mockEnabled bool
lastUpdate time.Time
updateInterval = 2 * time.Second
dataMu sync.RWMutex
mockData models.StateSnapshot
mockAlerts []models.Alert
mockConfig = DefaultConfig
enabled atomic.Bool
updateTicker *time.Ticker
stopUpdatesCh chan struct{}
)
const updateInterval = 2 * time.Second
func init() {
// Check if mock mode is enabled
mockEnabled = os.Getenv("PULSE_MOCK_MODE") == "true"
initialEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
if initialEnabled {
log.Info().Msg("Mock mode enabled at startup")
}
setEnabled(initialEnabled, true)
}
if mockEnabled {
log.Info().Msg("Mock mode enabled - using simulated data")
// IsMockEnabled returns whether mock mode is enabled.
func IsMockEnabled() bool {
return enabled.Load()
}
// Load configuration from env vars or use defaults
config := LoadMockConfig()
// SetEnabled enables or disables mock mode.
func SetEnabled(enable bool) {
setEnabled(enable, false)
}
// Generate initial mock data
mockData = GenerateMockData(config)
// Removed fake alert generation - real alert manager will handle this
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
lastUpdate = time.Now()
// ToggleMockMode enables or disables mock mode at runtime (backwards-compatible helper).
func ToggleMockMode(enable bool) {
SetEnabled(enable)
}
// Start update ticker
go func() {
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
func setEnabled(enable bool, fromInit bool) {
current := enabled.Load()
if current == enable {
// Still update env so other processes see the latest value when not invoked from init.
if !fromInit {
setEnvFlag(enable)
}
return
}
for range ticker.C {
if mockEnabled {
UpdateMetrics(&mockData, config)
// Removed fake alert regeneration
}
}
}()
if enable {
enableMockMode(fromInit)
} else {
disableMockMode()
}
if !fromInit {
setEnvFlag(enable)
}
}
// LoadMockConfig loads mock configuration from environment variables
func setEnvFlag(enable bool) {
if enable {
_ = os.Setenv("PULSE_MOCK_MODE", "true")
} else {
_ = os.Setenv("PULSE_MOCK_MODE", "false")
}
}
func enableMockMode(fromInit bool) {
config := LoadMockConfig()
dataMu.Lock()
mockConfig = config
mockData = GenerateMockData(config)
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
mockData.LastUpdate = time.Now()
enabled.Store(true)
startUpdateLoopLocked()
dataMu.Unlock()
log.Info().
Int("nodes", config.NodeCount).
Int("vms_per_node", config.VMsPerNode).
Int("lxcs_per_node", config.LXCsPerNode).
Bool("random_metrics", config.RandomMetrics).
Float64("stopped_percent", config.StoppedPercent).
Msg("Mock mode enabled")
if !fromInit {
log.Info().Msg("Mock data generator started")
}
}
func disableMockMode() {
dataMu.Lock()
if !enabled.Load() {
dataMu.Unlock()
return
}
enabled.Store(false)
stopUpdateLoopLocked()
mockData = models.StateSnapshot{}
mockAlerts = nil
dataMu.Unlock()
log.Info().Msg("Mock mode disabled")
}
func startUpdateLoopLocked() {
stopUpdateLoopLocked()
stopUpdatesCh = make(chan struct{})
updateTicker = time.NewTicker(updateInterval)
go func() {
for {
select {
case <-updateTicker.C:
cfg := GetConfig()
updateMetrics(cfg)
case <-stopUpdatesCh:
return
}
}
}()
}
func stopUpdateLoopLocked() {
if updateTicker != nil {
updateTicker.Stop()
updateTicker = nil
}
if stopUpdatesCh != nil {
close(stopUpdatesCh)
stopUpdatesCh = nil
}
}
func updateMetrics(cfg MockConfig) {
if !IsMockEnabled() {
return
}
dataMu.Lock()
defer dataMu.Unlock()
UpdateMetrics(&mockData, cfg)
mockData.LastUpdate = time.Now()
}
// GetConfig returns the current mock configuration.
func GetConfig() MockConfig {
dataMu.RLock()
defer dataMu.RUnlock()
return mockConfig
}
// LoadMockConfig loads mock configuration from environment variables.
func LoadMockConfig() MockConfig {
config := DefaultConfig
if val := os.Getenv("PULSE_MOCK_NODES"); val != "" {
if n, err := strconv.Atoi(val); err == nil {
if n, err := strconv.Atoi(val); err == nil && n > 0 {
config.NodeCount = n
}
}
if val := os.Getenv("PULSE_MOCK_VMS_PER_NODE"); val != "" {
if n, err := strconv.Atoi(val); err == nil {
if n, err := strconv.Atoi(val); err == nil && n >= 0 {
config.VMsPerNode = n
}
}
if val := os.Getenv("PULSE_MOCK_LXCS_PER_NODE"); val != "" {
if n, err := strconv.Atoi(val); err == nil {
if n, err := strconv.Atoi(val); err == nil && n >= 0 {
config.LXCsPerNode = n
}
}
@@ -81,84 +195,83 @@ func LoadMockConfig() MockConfig {
}
}
log.Info().
Int("nodes", config.NodeCount).
Int("vms_per_node", config.VMsPerNode).
Int("lxcs_per_node", config.LXCsPerNode).
Bool("random_metrics", config.RandomMetrics).
Float64("stopped_percent", config.StoppedPercent).
Msg("Mock configuration loaded")
return config
}
// IsMockEnabled returns whether mock mode is enabled
func IsMockEnabled() bool {
return mockEnabled
}
// GetMockState returns the current mock state snapshot
func GetMockState() models.StateSnapshot {
if !mockEnabled {
return models.StateSnapshot{}
// SetMockConfig updates the mock configuration dynamically and regenerates data when enabled.
func SetMockConfig(cfg MockConfig) {
dataMu.Lock()
mockConfig = cfg
if enabled.Load() {
mockData = GenerateMockData(cfg)
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
mockData.LastUpdate = time.Now()
}
// Return the current mock data
// Don't override alerts - let the real alert manager handle them
// mockData.ActiveAlerts = mockAlerts
return mockData
}
// ToggleMockMode enables or disables mock mode at runtime
func ToggleMockMode(enable bool) {
if enable && !mockEnabled {
mockEnabled = true
config := LoadMockConfig()
mockData = GenerateMockData(config)
// Removed fake alert generation
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
log.Info().
Int("history_count", len(mockAlertHistory)).
Msg("Mock mode enabled dynamically with alert history")
} else if !enable && mockEnabled {
mockEnabled = false
log.Info().Msg("Mock mode disabled dynamically")
}
}
// SetMockConfig updates the mock configuration dynamically
func SetMockConfig(nodeCount, vmsPerNode, lxcsPerNode int) {
if !mockEnabled {
return
}
config := MockConfig{
NodeCount: nodeCount,
VMsPerNode: vmsPerNode,
LXCsPerNode: lxcsPerNode,
RandomMetrics: true,
StoppedPercent: 0.2,
}
mockData = GenerateMockData(config)
// Removed fake alert generation
mockAlertHistory = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
dataMu.Unlock()
log.Info().
Int("nodes", nodeCount).
Int("vms", vmsPerNode).
Int("lxcs", lxcsPerNode).
Int("nodes", cfg.NodeCount).
Int("vms_per_node", cfg.VMsPerNode).
Int("lxcs_per_node", cfg.LXCsPerNode).
Bool("random_metrics", cfg.RandomMetrics).
Float64("stopped_percent", cfg.StoppedPercent).
Msg("Mock configuration updated")
}
// GetMockAlertHistory returns mock alert history
// GetMockState returns the current mock state snapshot.
func GetMockState() models.StateSnapshot {
if !IsMockEnabled() {
return models.StateSnapshot{}
}
dataMu.RLock()
defer dataMu.RUnlock()
return cloneState(mockData)
}
// GetMockAlertHistory returns mock alert history.
func GetMockAlertHistory(limit int) []models.Alert {
if !mockEnabled {
if !IsMockEnabled() {
return []models.Alert{}
}
if limit > 0 && limit < len(mockAlertHistory) {
return mockAlertHistory[:limit]
dataMu.RLock()
defer dataMu.RUnlock()
if limit > 0 && limit < len(mockAlerts) {
return append([]models.Alert(nil), mockAlerts[:limit]...)
}
return mockAlertHistory
return append([]models.Alert(nil), mockAlerts...)
}
func cloneState(state models.StateSnapshot) models.StateSnapshot {
copyState := models.StateSnapshot{
Nodes: append([]models.Node(nil), state.Nodes...),
VMs: append([]models.VM(nil), state.VMs...),
Containers: append([]models.Container(nil), state.Containers...),
Storage: append([]models.Storage(nil), state.Storage...),
PhysicalDisks: append([]models.PhysicalDisk(nil), state.PhysicalDisks...),
PBSInstances: append([]models.PBSInstance(nil), state.PBSInstances...),
PBSBackups: append([]models.PBSBackup(nil), state.PBSBackups...),
Metrics: append([]models.Metric(nil), state.Metrics...),
Performance: state.Performance,
Stats: state.Stats,
ActiveAlerts: append([]models.Alert(nil), state.ActiveAlerts...),
RecentlyResolved: append([]models.ResolvedAlert(nil), state.RecentlyResolved...),
LastUpdate: state.LastUpdate,
ConnectionHealth: make(map[string]bool, len(state.ConnectionHealth)),
}
copyState.PVEBackups = models.PVEBackups{
BackupTasks: append([]models.BackupTask(nil), state.PVEBackups.BackupTasks...),
StorageBackups: append([]models.StorageBackup(nil), state.PVEBackups.StorageBackups...),
GuestSnapshots: append([]models.GuestSnapshot(nil), state.PVEBackups.GuestSnapshots...),
}
for k, v := range state.ConnectionHealth {
copyState.ConnectionHealth[k] = v
}
return copyState
}
+95 -22
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"math"
"net"
"os"
"sort"
"strconv"
"strings"
@@ -68,6 +67,8 @@ type Monitor struct {
lastAuthAttempt map[string]time.Time // Track last auth attempt time
lastClusterCheck map[string]time.Time // Track last cluster check for standalone nodes
persistence *config.ConfigPersistence // Add persistence for saving updated configs
runtimeCtx context.Context // Context used while monitor is running
wsHub *websocket.Hub // Hub used for broadcasting state
}
// safePercentage calculates percentage safely, returning 0 if divisor is 0
@@ -110,6 +111,26 @@ func sortContent(content string) string {
// GetConnectionStatuses returns the current connection status for all nodes
func (m *Monitor) GetConnectionStatuses() map[string]bool {
if mock.IsMockEnabled() {
statuses := make(map[string]bool)
state := mock.GetMockState()
for _, node := range state.Nodes {
key := "pve-" + node.Name
statuses[key] = strings.ToLower(node.Status) == "online"
if node.Host != "" {
statuses[node.Host] = strings.ToLower(node.Status) == "online"
}
}
for _, pbsInst := range state.PBSInstances {
key := "pbs-" + pbsInst.Name
statuses[key] = strings.ToLower(pbsInst.Status) != "offline"
if pbsInst.Host != "" {
statuses[pbsInst.Host] = strings.ToLower(pbsInst.Status) != "offline"
}
}
return statuses
}
m.mu.RLock()
defer m.mu.RUnlock()
@@ -212,7 +233,7 @@ func New(cfg *config.Config) (*Monitor, error) {
}
// Check if mock mode is enabled before initializing clients
mockEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
mockEnabled := mock.IsMockEnabled()
if mockEnabled {
log.Info().Msg("Mock mode enabled - skipping PVE/PBS client initialization")
@@ -373,6 +394,11 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
Dur("pollingInterval", 10*time.Second).
Msg("Starting monitoring loop")
m.mu.Lock()
m.runtimeCtx = ctx
m.wsHub = wsHub
m.mu.Unlock()
// Initialize and start discovery service if enabled
if m.config.DiscoveryEnabled {
discoverySubnet := m.config.DiscoverySubnet
@@ -455,25 +481,23 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
broadcastTicker := time.NewTicker(pollingInterval)
defer broadcastTicker.Stop()
// Check if mock mode is enabled
mockEnabled := os.Getenv("PULSE_MOCK_MODE") == "true"
// Do an immediate poll on start (only if not in mock mode)
if !mockEnabled {
go m.poll(ctx, wsHub)
} else {
if mock.IsMockEnabled() {
log.Info().Msg("Mock mode enabled - skipping real node polling")
go m.checkMockAlerts()
} else {
go m.poll(ctx, wsHub)
}
for {
select {
case <-pollTicker.C:
// Start polling in a goroutine so it doesn't block the ticker (only if not in mock mode)
if !mockEnabled {
go m.poll(ctx, wsHub)
} else {
// In mock mode, still check alerts for mock data
if mock.IsMockEnabled() {
// In mock mode, keep synthetic alerts fresh
go m.checkMockAlerts()
} else {
// Poll real infrastructure
go m.poll(ctx, wsHub)
}
case <-broadcastTicker.C:
@@ -2759,6 +2783,53 @@ func (m *Monitor) GetState() models.StateSnapshot {
return m.state.GetSnapshot()
}
// SetMockMode switches between mock data and real infrastructure data at runtime.
func (m *Monitor) SetMockMode(enable bool) {
current := mock.IsMockEnabled()
if current == enable {
log.Info().Bool("mockMode", enable).Msg("Mock mode already in desired state")
return
}
if enable {
mock.SetEnabled(true)
m.alertManager.ClearActiveAlerts()
m.mu.Lock()
m.resetStateLocked()
m.mu.Unlock()
log.Info().Msg("Switched monitor to mock mode")
} else {
mock.SetEnabled(false)
m.alertManager.ClearActiveAlerts()
m.mu.Lock()
m.resetStateLocked()
m.mu.Unlock()
log.Info().Msg("Switched monitor to real data mode")
}
m.mu.RLock()
ctx := m.runtimeCtx
hub := m.wsHub
m.mu.RUnlock()
if hub != nil {
hub.BroadcastState(m.GetState())
}
if !enable && ctx != nil && hub != nil {
// Kick off an immediate poll to repopulate state with live data
go m.poll(ctx, hub)
}
}
func (m *Monitor) resetStateLocked() {
m.state = models.NewState()
m.state.Stats = models.Stats{
StartTime: m.startTime,
Version: "2.0.0-go",
}
}
// GetStartTime returns the monitor start time
func (m *Monitor) GetStartTime() time.Time {
return m.startTime
@@ -3362,21 +3433,23 @@ func (m *Monitor) checkMockAlerts() {
Msg("Checking alerts for mock data")
// Clean up alerts for nodes that no longer exist
// Use the mock state nodes since we haven't updated global state yet
existingNodes := make(map[string]bool)
for _, node := range state.Nodes {
existingNodes[node.Name] = true
if node.Host != "" {
existingNodes[node.Host] = true
}
}
// Also add any real nodes from the global state
allState := m.state.GetSnapshot()
for _, node := range allState.Nodes {
existingNodes[node.Name] = true
for _, pbsInst := range state.PBSInstances {
existingNodes[pbsInst.Name] = true
existingNodes["pbs-"+pbsInst.Name] = true
if pbsInst.Host != "" {
existingNodes[pbsInst.Host] = true
}
}
log.Info().
Int("mockNodes", len(state.Nodes)).
Int("stateNodes", len(allState.Nodes)).
Int("totalNodes", len(existingNodes)).
Msg("Collecting nodes for alert cleanup")
Int("trackedNodes", len(existingNodes)).
Msg("Collecting resources for alert cleanup in mock mode")
m.alertManager.CleanupAlertsForNodes(existingNodes)
// Limit how many guests we check per cycle to prevent blocking with large datasets