Pulse/internal/discovery/service.go
Pulse Monitor d6e93e2e2b feat: major improvements to cluster detection, auto-registration, and UI
Frontend:
- Enhanced cluster vs standalone node visual distinction in Settings
- Added glassmorphic style to all toast notifications for consistency
- Fixed test connection in edit modal to use stored encrypted credentials
- Added batch credential modal for bulk node operations
- Added network discovery modal with auto-subnet detection
- Improved notification system with dual toast/notification support
- Added event bus for component communication

Backend:
- Fixed duplicate toast notifications during auto-registration
- Fixed PBS auto-registration token extraction from JSON output
- Added network discovery service with background scanning
- Improved cluster detection with actual cluster name from API
- Added helper function to reduce code duplication in cluster detection
- Fixed host URL normalization in auto-registration
- Enhanced PBS client token authentication parsing

Bug Fixes:
- Fixed stacking toast notifications creating visual bugs
- Fixed PBS authentication failures after auto-registration
- Fixed network discovery not finding Proxmox servers
- Fixed test connection for existing nodes with encrypted tokens
- Removed duplicate WebSocket broadcasts for auto-registration events
2025-08-08 21:25:28 +00:00

197 lines
No EOL
4.4 KiB
Go

package discovery
import (
"context"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
"github.com/rs/zerolog/log"
)
// Service handles background network discovery
type Service struct {
scanner *discovery.Scanner
wsHub *websocket.Hub
cache *DiscoveryCache
interval time.Duration
subnet string
mu sync.RWMutex
lastScan time.Time
isScanning bool
stopChan chan struct{}
ctx context.Context
}
// DiscoveryCache stores the latest discovery results
type DiscoveryCache struct {
mu sync.RWMutex
result *discovery.DiscoveryResult
updated time.Time
}
// NewService creates a new discovery service
func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string) *Service {
if interval == 0 {
interval = 5 * time.Minute // Default to 5 minutes
}
if subnet == "" {
subnet = "auto"
}
return &Service{
scanner: discovery.NewScanner(),
wsHub: wsHub,
cache: &DiscoveryCache{},
interval: interval,
subnet: subnet,
stopChan: make(chan struct{}),
}
}
// Start begins the background discovery service
func (s *Service) Start(ctx context.Context) {
s.ctx = ctx
log.Info().
Dur("interval", s.interval).
Str("subnet", s.subnet).
Msg("Starting background discovery service")
// Do initial scan immediately
go s.performScan()
// Start background scanning loop
go s.scanLoop()
}
// Stop stops the background discovery service
func (s *Service) Stop() {
close(s.stopChan)
}
// scanLoop runs periodic scans
func (s *Service) scanLoop() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.performScan()
case <-s.stopChan:
log.Info().Msg("Stopping background discovery service")
return
case <-s.ctx.Done():
log.Info().Msg("Background discovery service context cancelled")
return
}
}
}
// performScan executes a network scan
func (s *Service) performScan() {
s.mu.Lock()
if s.isScanning {
s.mu.Unlock()
log.Debug().Msg("Discovery scan already in progress, skipping")
return
}
s.isScanning = true
s.mu.Unlock()
defer func() {
s.mu.Lock()
s.isScanning = false
s.lastScan = time.Now()
s.mu.Unlock()
}()
log.Info().Str("subnet", s.subnet).Msg("Starting background discovery scan")
// Create a context with timeout for the scan
scanCtx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
defer cancel()
// Perform the scan
result, err := s.scanner.DiscoverServers(scanCtx, s.subnet)
if err != nil {
log.Error().Err(err).Msg("Background discovery scan failed")
return
}
// Update cache
s.cache.mu.Lock()
s.cache.result = result
s.cache.updated = time.Now()
s.cache.mu.Unlock()
log.Info().
Int("servers", len(result.Servers)).
Int("errors", len(result.Errors)).
Msg("Background discovery scan completed")
// Send update via WebSocket
if s.wsHub != nil {
s.wsHub.Broadcast(websocket.Message{
Type: "discovery_update",
Data: map[string]interface{}{
"servers": result.Servers,
"errors": result.Errors,
"timestamp": time.Now().Unix(),
},
})
}
}
// GetCachedResult returns the cached discovery result
func (s *Service) GetCachedResult() (*discovery.DiscoveryResult, time.Time) {
s.cache.mu.RLock()
defer s.cache.mu.RUnlock()
if s.cache.result == nil {
return &discovery.DiscoveryResult{
Servers: []discovery.DiscoveredServer{},
Errors: []string{},
}, time.Time{}
}
return s.cache.result, s.cache.updated
}
// ForceRefresh triggers an immediate scan
func (s *Service) ForceRefresh() {
go s.performScan()
}
// SetInterval updates the scan interval
func (s *Service) SetInterval(interval time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
s.interval = interval
log.Info().Dur("interval", interval).Msg("Updated discovery scan interval")
}
// SetSubnet updates the subnet to scan
func (s *Service) SetSubnet(subnet string) {
s.mu.Lock()
defer s.mu.Unlock()
s.subnet = subnet
log.Info().Str("subnet", subnet).Msg("Updated discovery subnet")
// Trigger immediate rescan with new subnet
go s.performScan()
}
// GetStatus returns the current service status
func (s *Service) GetStatus() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
return map[string]interface{}{
"is_scanning": s.isScanning,
"last_scan": s.lastScan,
"interval": s.interval.String(),
"subnet": s.subnet,
}
}