phase 1: add unified resources to WebSocket state broadcasts
- Extended StateFrontend with Resources field containing unified resource data - Added ResourceFrontend and related types for frontend-compatible resource data - Extended ResourceStoreInterface to include GetAll() method - Monitor now injects resources into WebSocket broadcasts - Added helper method getResourcesForBroadcast() to convert resources to frontend format - All existing tests pass This enables the frontend to access unified resources via WebSocket state.
This commit is contained in:
parent
ef35120afb
commit
cdd2249e00
3 changed files with 330 additions and 3 deletions
|
|
@ -732,3 +732,141 @@ func zeroIfNegative(val int64) int64 {
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceToFrontend converts a resources.Resource to ResourceFrontend.
|
||||||
|
// This function is in models package to avoid circular imports.
|
||||||
|
// It takes individual fields rather than the whole Resource to avoid importing resources package.
|
||||||
|
type ResourceConvertInput struct {
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
DisplayName string
|
||||||
|
PlatformID string
|
||||||
|
PlatformType string
|
||||||
|
SourceType string
|
||||||
|
ParentID string
|
||||||
|
ClusterID string
|
||||||
|
Status string
|
||||||
|
CPU *ResourceMetricInput
|
||||||
|
Memory *ResourceMetricInput
|
||||||
|
Disk *ResourceMetricInput
|
||||||
|
NetworkRX int64
|
||||||
|
NetworkTX int64
|
||||||
|
HasNetwork bool
|
||||||
|
Temperature *float64
|
||||||
|
Uptime *int64
|
||||||
|
Tags []string
|
||||||
|
Labels map[string]string
|
||||||
|
LastSeenUnix int64
|
||||||
|
Alerts []ResourceAlertInput
|
||||||
|
Identity *ResourceIdentityInput
|
||||||
|
PlatformData map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMetricInput represents a metric value for resource conversion.
|
||||||
|
type ResourceMetricInput struct {
|
||||||
|
Current float64
|
||||||
|
Total *int64
|
||||||
|
Used *int64
|
||||||
|
Free *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceAlertInput struct {
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Level string
|
||||||
|
Message string
|
||||||
|
Value float64
|
||||||
|
Threshold float64
|
||||||
|
StartTimeUnix int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceIdentityInput struct {
|
||||||
|
Hostname string
|
||||||
|
MachineID string
|
||||||
|
IPs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertResourceToFrontend converts input to ResourceFrontend.
|
||||||
|
func ConvertResourceToFrontend(input ResourceConvertInput) ResourceFrontend {
|
||||||
|
rf := ResourceFrontend{
|
||||||
|
ID: input.ID,
|
||||||
|
Type: input.Type,
|
||||||
|
Name: input.Name,
|
||||||
|
DisplayName: input.DisplayName,
|
||||||
|
PlatformID: input.PlatformID,
|
||||||
|
PlatformType: input.PlatformType,
|
||||||
|
SourceType: input.SourceType,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
ClusterID: input.ClusterID,
|
||||||
|
Status: input.Status,
|
||||||
|
Temperature: input.Temperature,
|
||||||
|
Uptime: input.Uptime,
|
||||||
|
Tags: input.Tags,
|
||||||
|
Labels: input.Labels,
|
||||||
|
LastSeen: input.LastSeenUnix,
|
||||||
|
PlatformData: input.PlatformData,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert metrics
|
||||||
|
if input.CPU != nil {
|
||||||
|
rf.CPU = &ResourceMetricFrontend{
|
||||||
|
Current: input.CPU.Current,
|
||||||
|
Total: input.CPU.Total,
|
||||||
|
Used: input.CPU.Used,
|
||||||
|
Free: input.CPU.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Memory != nil {
|
||||||
|
rf.Memory = &ResourceMetricFrontend{
|
||||||
|
Current: input.Memory.Current,
|
||||||
|
Total: input.Memory.Total,
|
||||||
|
Used: input.Memory.Used,
|
||||||
|
Free: input.Memory.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Disk != nil {
|
||||||
|
rf.Disk = &ResourceMetricFrontend{
|
||||||
|
Current: input.Disk.Current,
|
||||||
|
Total: input.Disk.Total,
|
||||||
|
Used: input.Disk.Used,
|
||||||
|
Free: input.Disk.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.HasNetwork {
|
||||||
|
rf.Network = &ResourceNetworkFrontend{
|
||||||
|
RXBytes: input.NetworkRX,
|
||||||
|
TXBytes: input.NetworkTX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert alerts
|
||||||
|
if len(input.Alerts) > 0 {
|
||||||
|
rf.Alerts = make([]ResourceAlertFrontend, len(input.Alerts))
|
||||||
|
for i, a := range input.Alerts {
|
||||||
|
rf.Alerts[i] = ResourceAlertFrontend{
|
||||||
|
ID: a.ID,
|
||||||
|
Type: a.Type,
|
||||||
|
Level: a.Level,
|
||||||
|
Message: a.Message,
|
||||||
|
Value: a.Value,
|
||||||
|
Threshold: a.Threshold,
|
||||||
|
StartTime: a.StartTimeUnix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert identity
|
||||||
|
if input.Identity != nil {
|
||||||
|
rf.Identity = &ResourceIdentityFrontend{
|
||||||
|
Hostname: input.Identity.Hostname,
|
||||||
|
MachineID: input.Identity.MachineID,
|
||||||
|
IPs: input.Identity.IPs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rf
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -446,4 +446,78 @@ type StateFrontend struct {
|
||||||
Stats map[string]any `json:"stats"` // Empty object for now
|
Stats map[string]any `json:"stats"` // Empty object for now
|
||||||
LastUpdate int64 `json:"lastUpdate"` // Unix timestamp
|
LastUpdate int64 `json:"lastUpdate"` // Unix timestamp
|
||||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting
|
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting
|
||||||
|
// Unified resources - the new way to access all monitored entities
|
||||||
|
Resources []ResourceFrontend `json:"resources,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceFrontend is the frontend representation of a unified Resource.
|
||||||
|
// This mirrors resources.Resource but with time.Time converted to Unix milliseconds.
|
||||||
|
type ResourceFrontend struct {
|
||||||
|
// Identity
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
|
||||||
|
// Platform/Source
|
||||||
|
PlatformID string `json:"platformId"`
|
||||||
|
PlatformType string `json:"platformType"`
|
||||||
|
SourceType string `json:"sourceType"`
|
||||||
|
|
||||||
|
// Hierarchy
|
||||||
|
ParentID string `json:"parentId,omitempty"`
|
||||||
|
ClusterID string `json:"clusterId,omitempty"`
|
||||||
|
|
||||||
|
// Universal Metrics
|
||||||
|
Status string `json:"status"`
|
||||||
|
CPU *ResourceMetricFrontend `json:"cpu,omitempty"`
|
||||||
|
Memory *ResourceMetricFrontend `json:"memory,omitempty"`
|
||||||
|
Disk *ResourceMetricFrontend `json:"disk,omitempty"`
|
||||||
|
Network *ResourceNetworkFrontend `json:"network,omitempty"`
|
||||||
|
Temperature *float64 `json:"temperature,omitempty"`
|
||||||
|
Uptime *int64 `json:"uptime,omitempty"`
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
LastSeen int64 `json:"lastSeen"` // Unix milliseconds
|
||||||
|
Alerts []ResourceAlertFrontend `json:"alerts,omitempty"`
|
||||||
|
|
||||||
|
// Identity for deduplication
|
||||||
|
Identity *ResourceIdentityFrontend `json:"identity,omitempty"`
|
||||||
|
|
||||||
|
// Platform-specific data (JSON blob)
|
||||||
|
PlatformData map[string]any `json:"platformData,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMetricFrontend represents a metric value for the frontend.
|
||||||
|
type ResourceMetricFrontend struct {
|
||||||
|
Current float64 `json:"current"`
|
||||||
|
Total *int64 `json:"total,omitempty"`
|
||||||
|
Used *int64 `json:"used,omitempty"`
|
||||||
|
Free *int64 `json:"free,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceNetworkFrontend represents network metrics for the frontend.
|
||||||
|
type ResourceNetworkFrontend struct {
|
||||||
|
RXBytes int64 `json:"rxBytes"`
|
||||||
|
TXBytes int64 `json:"txBytes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceAlertFrontend represents an alert on a resource.
|
||||||
|
type ResourceAlertFrontend struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
StartTime int64 `json:"startTime"` // Unix milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceIdentityFrontend contains identity info for deduplication.
|
||||||
|
type ResourceIdentityFrontend struct {
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
MachineID string `json:"machineId,omitempty"`
|
||||||
|
IPs []string `json:"ips,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
stderrors "errors"
|
stderrors "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
@ -28,6 +29,7 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/resources"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/system"
|
"github.com/rcourtman/pulse-go-rewrite/internal/system"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/types"
|
"github.com/rcourtman/pulse-go-rewrite/internal/types"
|
||||||
|
|
@ -80,7 +82,7 @@ type PVEClientInterface interface {
|
||||||
GetCephDF(ctx context.Context) (*proxmox.CephDF, error)
|
GetCephDF(ctx context.Context) (*proxmox.CephDF, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceStoreInterface provides methods for polling optimization.
|
// ResourceStoreInterface provides methods for polling optimization and resource access.
|
||||||
// When an agent is monitoring a node, we can reduce API polling for that node.
|
// When an agent is monitoring a node, we can reduce API polling for that node.
|
||||||
type ResourceStoreInterface interface {
|
type ResourceStoreInterface interface {
|
||||||
// ShouldSkipAPIPolling returns true if API polling should be skipped for the hostname
|
// ShouldSkipAPIPolling returns true if API polling should be skipped for the hostname
|
||||||
|
|
@ -89,6 +91,8 @@ type ResourceStoreInterface interface {
|
||||||
// GetPollingRecommendations returns a map of hostname -> polling multiplier.
|
// GetPollingRecommendations returns a map of hostname -> polling multiplier.
|
||||||
// 0 = skip entirely, 0.5 = half frequency, 1 = normal
|
// 0 = skip entirely, 0.5 = half frequency, 1 = normal
|
||||||
GetPollingRecommendations() map[string]float64
|
GetPollingRecommendations() map[string]float64
|
||||||
|
// GetAll returns all resources in the store (for WebSocket broadcasts)
|
||||||
|
GetAll() []resources.Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNodeDisplayName(instance *config.PVEInstance, nodeName string) string {
|
func getNodeDisplayName(instance *config.PVEInstance, nodeName string) string {
|
||||||
|
|
@ -3285,7 +3289,10 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||||
Int("physicalDisks", len(state.PhysicalDisks)).
|
Int("physicalDisks", len(state.PhysicalDisks)).
|
||||||
Msg("Broadcasting state update (ticker)")
|
Msg("Broadcasting state update (ticker)")
|
||||||
// Convert to frontend format before broadcasting (converts time.Time to int64, etc.)
|
// Convert to frontend format before broadcasting (converts time.Time to int64, etc.)
|
||||||
wsHub.BroadcastState(state.ToFrontend())
|
frontendState := state.ToFrontend()
|
||||||
|
// Inject unified resources if resource store is available
|
||||||
|
frontendState.Resources = m.getResourcesForBroadcast()
|
||||||
|
wsHub.BroadcastState(frontendState)
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Info().Msg("Monitoring loop stopped")
|
log.Info().Msg("Monitoring loop stopped")
|
||||||
|
|
@ -6894,7 +6901,9 @@ func (m *Monitor) SetMockMode(enable bool) {
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
|
||||||
if hub != nil {
|
if hub != nil {
|
||||||
hub.BroadcastState(m.GetState().ToFrontend())
|
frontendState := m.GetState().ToFrontend()
|
||||||
|
frontendState.Resources = m.getResourcesForBroadcast()
|
||||||
|
hub.BroadcastState(frontendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !enable && ctx != nil && hub != nil {
|
if !enable && ctx != nil && hub != nil {
|
||||||
|
|
@ -7048,6 +7057,112 @@ func (m *Monitor) shouldSkipNodeMetrics(nodeName string) bool {
|
||||||
return should
|
return should
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getResourcesForBroadcast retrieves all resources from the store and converts them to frontend format.
|
||||||
|
// Returns nil if no resource store is configured.
|
||||||
|
func (m *Monitor) getResourcesForBroadcast() []models.ResourceFrontend {
|
||||||
|
m.mu.RLock()
|
||||||
|
store := m.resourceStore
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allResources := store.GetAll()
|
||||||
|
if len(allResources) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]models.ResourceFrontend, len(allResources))
|
||||||
|
for i, r := range allResources {
|
||||||
|
input := models.ResourceConvertInput{
|
||||||
|
ID: r.ID,
|
||||||
|
Type: string(r.Type),
|
||||||
|
Name: r.Name,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
PlatformID: r.PlatformID,
|
||||||
|
PlatformType: string(r.PlatformType),
|
||||||
|
SourceType: string(r.SourceType),
|
||||||
|
ParentID: r.ParentID,
|
||||||
|
ClusterID: r.ClusterID,
|
||||||
|
Status: string(r.Status),
|
||||||
|
Temperature: r.Temperature,
|
||||||
|
Uptime: r.Uptime,
|
||||||
|
Tags: r.Tags,
|
||||||
|
Labels: r.Labels,
|
||||||
|
LastSeenUnix: r.LastSeen.UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert metrics
|
||||||
|
if r.CPU != nil {
|
||||||
|
input.CPU = &models.ResourceMetricInput{
|
||||||
|
Current: r.CPU.Current,
|
||||||
|
Total: r.CPU.Total,
|
||||||
|
Used: r.CPU.Used,
|
||||||
|
Free: r.CPU.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Memory != nil {
|
||||||
|
input.Memory = &models.ResourceMetricInput{
|
||||||
|
Current: r.Memory.Current,
|
||||||
|
Total: r.Memory.Total,
|
||||||
|
Used: r.Memory.Used,
|
||||||
|
Free: r.Memory.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Disk != nil {
|
||||||
|
input.Disk = &models.ResourceMetricInput{
|
||||||
|
Current: r.Disk.Current,
|
||||||
|
Total: r.Disk.Total,
|
||||||
|
Used: r.Disk.Used,
|
||||||
|
Free: r.Disk.Free,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Network != nil {
|
||||||
|
input.HasNetwork = true
|
||||||
|
input.NetworkRX = r.Network.RXBytes
|
||||||
|
input.NetworkTX = r.Network.TXBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert alerts
|
||||||
|
if len(r.Alerts) > 0 {
|
||||||
|
input.Alerts = make([]models.ResourceAlertInput, len(r.Alerts))
|
||||||
|
for j, a := range r.Alerts {
|
||||||
|
input.Alerts[j] = models.ResourceAlertInput{
|
||||||
|
ID: a.ID,
|
||||||
|
Type: a.Type,
|
||||||
|
Level: a.Level,
|
||||||
|
Message: a.Message,
|
||||||
|
Value: a.Value,
|
||||||
|
Threshold: a.Threshold,
|
||||||
|
StartTimeUnix: a.StartTime.UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert identity
|
||||||
|
if r.Identity != nil {
|
||||||
|
input.Identity = &models.ResourceIdentityInput{
|
||||||
|
Hostname: r.Identity.Hostname,
|
||||||
|
MachineID: r.Identity.MachineID,
|
||||||
|
IPs: r.Identity.IPs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert platform data from json.RawMessage to map
|
||||||
|
if len(r.PlatformData) > 0 {
|
||||||
|
var platformMap map[string]any
|
||||||
|
if err := json.Unmarshal(r.PlatformData, &platformMap); err == nil {
|
||||||
|
input.PlatformData = platformMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = models.ConvertResourceToFrontend(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// pollStorageBackupsWithNodes polls backups using a provided nodes list to avoid duplicate GetNodes calls
|
// pollStorageBackupsWithNodes polls backups using a provided nodes list to avoid duplicate GetNodes calls
|
||||||
func (m *Monitor) pollStorageBackupsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node, nodeEffectiveStatus map[string]string) {
|
func (m *Monitor) pollStorageBackupsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node, nodeEffectiveStatus map[string]string) {
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue