Pulse/internal/models/models.go
Pulse Monitor 8492b0932d fix: dashboard now uses actual configured host URLs for node links
addresses #306 - The dashboard and storage views were hardcoding port 8006 for node links,
but now they properly use the host URLs from the node configuration. This ensures users
are redirected to the correct URL when clicking on node names, respecting custom ports
and protocols configured in the settings.

- Added host field to Node struct in Go models
- Updated monitor.go to populate host field from instance config
- Added host field to TypeScript Node interface
- Modified Dashboard and Storage components to use nodeHostMap for correct URLs
- Falls back to old behavior if host field is not available
2025-08-12 14:28:19 +00:00

733 lines
23 KiB
Go

package models
import (
"sort"
"strings"
"sync"
"time"
)
// State represents the current state of all monitored resources
type State struct {
mu sync.RWMutex
Nodes []Node `json:"nodes"`
VMs []VM `json:"vms"`
Containers []Container `json:"containers"`
Storage []Storage `json:"storage"`
PBSInstances []PBSInstance `json:"pbs"`
PBSBackups []PBSBackup `json:"pbsBackups"`
Metrics []Metric `json:"metrics"`
PVEBackups PVEBackups `json:"pveBackups"`
Performance Performance `json:"performance"`
ConnectionHealth map[string]bool `json:"connectionHealth"`
Stats Stats `json:"stats"`
ActiveAlerts []Alert `json:"activeAlerts"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"`
}
// Alert represents an active alert (simplified for State)
type Alert struct {
ID string `json:"id"`
Type string `json:"type"`
Level string `json:"level"`
ResourceID string `json:"resourceId"`
ResourceName string `json:"resourceName"`
Node string `json:"node"`
Instance string `json:"instance"`
Message string `json:"message"`
Value float64 `json:"value"`
Threshold float64 `json:"threshold"`
StartTime time.Time `json:"startTime"`
Acknowledged bool `json:"acknowledged"`
}
// ResolvedAlert represents a recently resolved alert
type ResolvedAlert struct {
Alert
ResolvedTime time.Time `json:"resolvedTime"`
}
// Node represents a Proxmox VE node
type Node struct {
ID string `json:"id"`
Name string `json:"name"`
Instance string `json:"instance"`
Host string `json:"host"` // Full host URL from config
Status string `json:"status"`
Type string `json:"type"`
CPU float64 `json:"cpu"`
Memory Memory `json:"memory"`
Disk Disk `json:"disk"`
Uptime int64 `json:"uptime"`
LoadAverage []float64 `json:"loadAverage"`
KernelVersion string `json:"kernelVersion"`
PVEVersion string `json:"pveVersion"`
CPUInfo CPUInfo `json:"cpuInfo"`
LastSeen time.Time `json:"lastSeen"`
ConnectionHealth string `json:"connectionHealth"`
}
// VM represents a virtual machine
type VM struct {
ID string `json:"id"`
VMID int `json:"vmid"`
Name string `json:"name"`
Node string `json:"node"`
Instance string `json:"instance"`
Status string `json:"status"`
Type string `json:"type"`
CPU float64 `json:"cpu"`
CPUs int `json:"cpus"`
Memory Memory `json:"memory"`
Disk Disk `json:"disk"`
NetworkIn int64 `json:"networkIn"`
NetworkOut int64 `json:"networkOut"`
DiskRead int64 `json:"diskRead"`
DiskWrite int64 `json:"diskWrite"`
Uptime int64 `json:"uptime"`
Template bool `json:"template"`
LastBackup time.Time `json:"lastBackup,omitempty"`
Tags []string `json:"tags,omitempty"`
Lock string `json:"lock,omitempty"`
LastSeen time.Time `json:"lastSeen"`
}
// Container represents an LXC container
type Container struct {
ID string `json:"id"`
VMID int `json:"vmid"`
Name string `json:"name"`
Node string `json:"node"`
Instance string `json:"instance"`
Status string `json:"status"`
Type string `json:"type"`
CPU float64 `json:"cpu"`
CPUs int `json:"cpus"`
Memory Memory `json:"memory"`
Disk Disk `json:"disk"`
NetworkIn int64 `json:"networkIn"`
NetworkOut int64 `json:"networkOut"`
DiskRead int64 `json:"diskRead"`
DiskWrite int64 `json:"diskWrite"`
Uptime int64 `json:"uptime"`
Template bool `json:"template"`
LastBackup time.Time `json:"lastBackup,omitempty"`
Tags []string `json:"tags,omitempty"`
Lock string `json:"lock,omitempty"`
LastSeen time.Time `json:"lastSeen"`
}
// Storage represents a storage resource
type Storage struct {
ID string `json:"id"`
Name string `json:"name"`
Node string `json:"node"`
Instance string `json:"instance"`
Type string `json:"type"`
Status string `json:"status"`
Total int64 `json:"total"`
Used int64 `json:"used"`
Free int64 `json:"free"`
Usage float64 `json:"usage"`
Content string `json:"content"`
Shared bool `json:"shared"`
Enabled bool `json:"enabled"`
Active bool `json:"active"`
}
// PBSInstance represents a Proxmox Backup Server instance
type PBSInstance struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Status string `json:"status"`
Version string `json:"version"`
CPU float64 `json:"cpu"` // CPU usage percentage
Memory float64 `json:"memory"` // Memory usage percentage
MemoryUsed int64 `json:"memoryUsed"` // Memory used in bytes
MemoryTotal int64 `json:"memoryTotal"` // Total memory in bytes
Uptime int64 `json:"uptime"` // Uptime in seconds
Datastores []PBSDatastore `json:"datastores"`
BackupJobs []PBSBackupJob `json:"backupJobs"`
SyncJobs []PBSSyncJob `json:"syncJobs"`
VerifyJobs []PBSVerifyJob `json:"verifyJobs"`
PruneJobs []PBSPruneJob `json:"pruneJobs"`
GarbageJobs []PBSGarbageJob `json:"garbageJobs"`
ConnectionHealth string `json:"connectionHealth"`
LastSeen time.Time `json:"lastSeen"`
}
// PBSDatastore represents a PBS datastore
type PBSDatastore struct {
Name string `json:"name"`
Total int64 `json:"total"`
Used int64 `json:"used"`
Free int64 `json:"free"`
Usage float64 `json:"usage"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Namespaces []PBSNamespace `json:"namespaces,omitempty"`
}
// PBSNamespace represents a PBS namespace
type PBSNamespace struct {
Path string `json:"path"`
Parent string `json:"parent,omitempty"`
Depth int `json:"depth"`
}
// PBSBackup represents a backup stored on PBS
type PBSBackup struct {
ID string `json:"id"` // Unique ID combining PBS instance, namespace, type, vmid, and time
Instance string `json:"instance"` // PBS instance name
Datastore string `json:"datastore"`
Namespace string `json:"namespace"`
BackupType string `json:"backupType"` // "vm" or "ct"
VMID string `json:"vmid"`
BackupTime time.Time `json:"backupTime"`
Size int64 `json:"size"`
Protected bool `json:"protected"`
Verified bool `json:"verified"`
Comment string `json:"comment,omitempty"`
Files []string `json:"files,omitempty"`
}
// PBSBackupJob represents a PBS backup job
type PBSBackupJob struct {
ID string `json:"id"`
Store string `json:"store"`
Type string `json:"type"`
VMID string `json:"vmid,omitempty"`
LastBackup time.Time `json:"lastBackup"`
NextRun time.Time `json:"nextRun,omitempty"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
// PBSSyncJob represents a PBS sync job
type PBSSyncJob struct {
ID string `json:"id"`
Store string `json:"store"`
Remote string `json:"remote"`
Status string `json:"status"`
LastSync time.Time `json:"lastSync"`
NextRun time.Time `json:"nextRun,omitempty"`
Error string `json:"error,omitempty"`
}
// PBSVerifyJob represents a PBS verification job
type PBSVerifyJob struct {
ID string `json:"id"`
Store string `json:"store"`
Status string `json:"status"`
LastVerify time.Time `json:"lastVerify"`
NextRun time.Time `json:"nextRun,omitempty"`
Error string `json:"error,omitempty"`
}
// PBSPruneJob represents a PBS prune job
type PBSPruneJob struct {
ID string `json:"id"`
Store string `json:"store"`
Status string `json:"status"`
LastPrune time.Time `json:"lastPrune"`
NextRun time.Time `json:"nextRun,omitempty"`
Error string `json:"error,omitempty"`
}
// PBSGarbageJob represents a PBS garbage collection job
type PBSGarbageJob struct {
ID string `json:"id"`
Store string `json:"store"`
Status string `json:"status"`
LastGarbage time.Time `json:"lastGarbage"`
NextRun time.Time `json:"nextRun,omitempty"`
RemovedBytes int64 `json:"removedBytes,omitempty"`
Error string `json:"error,omitempty"`
}
// Memory represents memory usage
type Memory struct {
Total int64 `json:"total"`
Used int64 `json:"used"`
Free int64 `json:"free"`
Usage float64 `json:"usage"`
}
// Disk represents disk usage
type Disk struct {
Total int64 `json:"total"`
Used int64 `json:"used"`
Free int64 `json:"free"`
Usage float64 `json:"usage"`
}
// CPUInfo represents CPU information
type CPUInfo struct {
Model string `json:"model"`
Cores int `json:"cores"`
Sockets int `json:"sockets"`
MHz string `json:"mhz"`
}
// Metric represents a time-series metric
type Metric struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
ID string `json:"id"`
Values map[string]interface{} `json:"values"`
}
// PVEBackups represents PVE backup information
type PVEBackups struct {
BackupTasks []BackupTask `json:"backupTasks"`
StorageBackups []StorageBackup `json:"storageBackups"`
GuestSnapshots []GuestSnapshot `json:"guestSnapshots"`
}
// BackupTask represents a PVE backup task
type BackupTask struct {
ID string `json:"id"`
Node string `json:"node"`
Type string `json:"type"`
VMID int `json:"vmid"`
Status string `json:"status"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime,omitempty"`
Size int64 `json:"size,omitempty"`
Error string `json:"error,omitempty"`
}
// StorageBackup represents a backup file in storage
type StorageBackup struct {
ID string `json:"id"`
Storage string `json:"storage"`
Node string `json:"node"`
Type string `json:"type"`
VMID int `json:"vmid"`
Time time.Time `json:"time"`
CTime int64 `json:"ctime"` // Unix timestamp for compatibility
Size int64 `json:"size"`
Format string `json:"format"`
Notes string `json:"notes,omitempty"`
Protected bool `json:"protected"`
Volid string `json:"volid"` // Volume ID for compatibility
IsPBS bool `json:"isPBS"` // Indicates if backup is on PBS storage
Verified bool `json:"verified"` // PBS verification status
Verification string `json:"verification,omitempty"` // Verification details
}
// GuestSnapshot represents a VM/CT snapshot
type GuestSnapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Node string `json:"node"`
Type string `json:"type"`
VMID int `json:"vmid"`
Time time.Time `json:"time"`
Description string `json:"description,omitempty"`
Parent string `json:"parent,omitempty"`
VMState bool `json:"vmstate"`
}
// Performance represents performance metrics
type Performance struct {
APICallDuration map[string]float64 `json:"apiCallDuration"`
LastPollDuration float64 `json:"lastPollDuration"`
PollingStartTime time.Time `json:"pollingStartTime"`
TotalAPICalls int `json:"totalApiCalls"`
FailedAPICalls int `json:"failedApiCalls"`
}
// Stats represents runtime statistics
type Stats struct {
StartTime time.Time `json:"startTime"`
Uptime int64 `json:"uptime"`
PollingCycles int `json:"pollingCycles"`
WebSocketClients int `json:"webSocketClients"`
Version string `json:"version"`
}
// NewState creates a new State instance
func NewState() *State {
return &State{
Nodes: make([]Node, 0),
VMs: make([]VM, 0),
Containers: make([]Container, 0),
Storage: make([]Storage, 0),
PBSInstances: make([]PBSInstance, 0),
PBSBackups: make([]PBSBackup, 0),
Metrics: make([]Metric, 0),
PVEBackups: PVEBackups{
BackupTasks: make([]BackupTask, 0),
StorageBackups: make([]StorageBackup, 0),
GuestSnapshots: make([]GuestSnapshot, 0),
},
ConnectionHealth: make(map[string]bool),
ActiveAlerts: make([]Alert, 0),
RecentlyResolved: make([]ResolvedAlert, 0),
LastUpdate: time.Now(),
}
}
// UpdateActiveAlerts updates the active alerts in the state
func (s *State) UpdateActiveAlerts(alerts []Alert) {
s.mu.Lock()
defer s.mu.Unlock()
s.ActiveAlerts = alerts
}
// UpdateRecentlyResolved updates the recently resolved alerts in the state
func (s *State) UpdateRecentlyResolved(resolved []ResolvedAlert) {
s.mu.Lock()
defer s.mu.Unlock()
s.RecentlyResolved = resolved
}
// UpdateNodes updates the nodes in the state
func (s *State) UpdateNodes(nodes []Node) {
s.mu.Lock()
defer s.mu.Unlock()
// Sort nodes by name to ensure consistent ordering
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Name < nodes[j].Name
})
s.Nodes = nodes
s.LastUpdate = time.Now()
}
// UpdateNodesForInstance updates nodes for a specific instance, merging with existing nodes
func (s *State) UpdateNodesForInstance(instanceName string, nodes []Node) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing nodes, excluding those from this instance
nodeMap := make(map[string]Node)
for _, node := range s.Nodes {
if node.Instance != instanceName {
nodeMap[node.ID] = node
}
}
// Add or update nodes from this instance
for _, node := range nodes {
nodeMap[node.ID] = node
}
// Convert map back to slice
newNodes := make([]Node, 0, len(nodeMap))
for _, node := range nodeMap {
newNodes = append(newNodes, node)
}
// Sort nodes by name to ensure consistent ordering
sort.Slice(newNodes, func(i, j int) bool {
return newNodes[i].Name < newNodes[j].Name
})
s.Nodes = newNodes
s.LastUpdate = time.Now()
}
// UpdateVMs updates the VMs in the state
func (s *State) UpdateVMs(vms []VM) {
s.mu.Lock()
defer s.mu.Unlock()
s.VMs = vms
s.LastUpdate = time.Now()
}
// UpdateVMsForInstance updates VMs for a specific instance, merging with existing VMs
func (s *State) UpdateVMsForInstance(instanceName string, vms []VM) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing VMs, excluding those from this instance
vmMap := make(map[string]VM)
for _, vm := range s.VMs {
if vm.Instance != instanceName {
vmMap[vm.ID] = vm
}
}
// Add or update VMs from this instance
for _, vm := range vms {
vmMap[vm.ID] = vm
}
// Convert map back to slice
newVMs := make([]VM, 0, len(vmMap))
for _, vm := range vmMap {
newVMs = append(newVMs, vm)
}
// Sort VMs by VMID to ensure consistent ordering
sort.Slice(newVMs, func(i, j int) bool {
return newVMs[i].VMID < newVMs[j].VMID
})
s.VMs = newVMs
s.LastUpdate = time.Now()
}
// UpdateContainers updates the containers in the state
func (s *State) UpdateContainers(containers []Container) {
s.mu.Lock()
defer s.mu.Unlock()
s.Containers = containers
s.LastUpdate = time.Now()
}
// UpdateContainersForInstance updates containers for a specific instance, merging with existing containers
func (s *State) UpdateContainersForInstance(instanceName string, containers []Container) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing containers, excluding those from this instance
containerMap := make(map[string]Container)
for _, container := range s.Containers {
if container.Instance != instanceName {
containerMap[container.ID] = container
}
}
// Add or update containers from this instance
for _, container := range containers {
containerMap[container.ID] = container
}
// Convert map back to slice
newContainers := make([]Container, 0, len(containerMap))
for _, container := range containerMap {
newContainers = append(newContainers, container)
}
// Sort containers by VMID to ensure consistent ordering
sort.Slice(newContainers, func(i, j int) bool {
return newContainers[i].VMID < newContainers[j].VMID
})
s.Containers = newContainers
s.LastUpdate = time.Now()
}
// UpdateStorage updates the storage in the state
func (s *State) UpdateStorage(storage []Storage) {
s.mu.Lock()
defer s.mu.Unlock()
s.Storage = storage
s.LastUpdate = time.Now()
}
// UpdateStorageForInstance updates storage for a specific instance, merging with existing storage
func (s *State) UpdateStorageForInstance(instanceName string, storage []Storage) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing storage, excluding those from this instance
storageMap := make(map[string]Storage)
for _, st := range s.Storage {
if st.Instance != instanceName {
storageMap[st.ID] = st
}
}
// Add or update storage from this instance
for _, st := range storage {
storageMap[st.ID] = st
}
// Convert map back to slice
newStorage := make([]Storage, 0, len(storageMap))
for _, st := range storageMap {
newStorage = append(newStorage, st)
}
// Sort storage by name to ensure consistent ordering
sort.Slice(newStorage, func(i, j int) bool {
if newStorage[i].Instance == newStorage[j].Instance {
return newStorage[i].Name < newStorage[j].Name
}
return newStorage[i].Instance < newStorage[j].Instance
})
s.Storage = newStorage
s.LastUpdate = time.Now()
}
// UpdatePBSInstances updates the PBS instances in the state
func (s *State) UpdatePBSInstances(instances []PBSInstance) {
s.mu.Lock()
defer s.mu.Unlock()
s.PBSInstances = instances
s.LastUpdate = time.Now()
}
// UpdatePBSInstance updates a single PBS instance in the state, merging with existing instances
func (s *State) UpdatePBSInstance(instance PBSInstance) {
s.mu.Lock()
defer s.mu.Unlock()
// Find and update existing instance or append new one
found := false
for i, existing := range s.PBSInstances {
if existing.ID == instance.ID {
s.PBSInstances[i] = instance
found = true
break
}
}
if !found {
s.PBSInstances = append(s.PBSInstances, instance)
}
s.LastUpdate = time.Now()
}
// UpdateBackupTasksForInstance updates backup tasks for a specific instance, merging with existing tasks
func (s *State) UpdateBackupTasksForInstance(instanceName string, tasks []BackupTask) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing tasks, excluding those from this instance
taskMap := make(map[string]BackupTask)
for _, task := range s.PVEBackups.BackupTasks {
// Check if task ID contains the instance name
if !strings.HasPrefix(task.ID, instanceName+"-") {
taskMap[task.ID] = task
}
}
// Add or update tasks from this instance
for _, task := range tasks {
taskMap[task.ID] = task
}
// Convert map back to slice
newTasks := make([]BackupTask, 0, len(taskMap))
for _, task := range taskMap {
newTasks = append(newTasks, task)
}
// Sort by start time descending
sort.Slice(newTasks, func(i, j int) bool {
return newTasks[i].StartTime.After(newTasks[j].StartTime)
})
s.PVEBackups.BackupTasks = newTasks
s.LastUpdate = time.Now()
}
// UpdateStorageBackupsForInstance updates storage backups for a specific instance, merging with existing backups
func (s *State) UpdateStorageBackupsForInstance(instanceName string, backups []StorageBackup) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing backups, excluding those from this instance
backupMap := make(map[string]StorageBackup)
for _, backup := range s.PVEBackups.StorageBackups {
// Check if backup ID contains the instance name
if !strings.HasPrefix(backup.ID, instanceName+"-") {
backupMap[backup.ID] = backup
}
}
// Add or update backups from this instance
for _, backup := range backups {
backupMap[backup.ID] = backup
}
// Convert map back to slice
newBackups := make([]StorageBackup, 0, len(backupMap))
for _, backup := range backupMap {
newBackups = append(newBackups, backup)
}
// Sort by time descending
sort.Slice(newBackups, func(i, j int) bool {
return newBackups[i].Time.After(newBackups[j].Time)
})
s.PVEBackups.StorageBackups = newBackups
s.LastUpdate = time.Now()
}
// UpdateGuestSnapshotsForInstance updates guest snapshots for a specific instance, merging with existing snapshots
func (s *State) UpdateGuestSnapshotsForInstance(instanceName string, snapshots []GuestSnapshot) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing snapshots, excluding those from this instance
snapshotMap := make(map[string]GuestSnapshot)
for _, snapshot := range s.PVEBackups.GuestSnapshots {
// Check if snapshot ID contains the instance name
if !strings.HasPrefix(snapshot.ID, instanceName+"-") {
snapshotMap[snapshot.ID] = snapshot
}
}
// Add or update snapshots from this instance
for _, snapshot := range snapshots {
snapshotMap[snapshot.ID] = snapshot
}
// Convert map back to slice
newSnapshots := make([]GuestSnapshot, 0, len(snapshotMap))
for _, snapshot := range snapshotMap {
newSnapshots = append(newSnapshots, snapshot)
}
// Sort by time descending
sort.Slice(newSnapshots, func(i, j int) bool {
return newSnapshots[i].Time.After(newSnapshots[j].Time)
})
s.PVEBackups.GuestSnapshots = newSnapshots
s.LastUpdate = time.Now()
}
// SetConnectionHealth updates the connection health for an instance
func (s *State) SetConnectionHealth(instanceID string, healthy bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.ConnectionHealth[instanceID] = healthy
}
// UpdatePBSBackups updates PBS backups for a specific instance
func (s *State) UpdatePBSBackups(instanceName string, backups []PBSBackup) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a map of existing backups excluding ones from this instance
backupMap := make(map[string]PBSBackup)
for _, backup := range s.PBSBackups {
if backup.Instance != instanceName {
backupMap[backup.ID] = backup
}
}
// Add new backups from this instance
for _, backup := range backups {
backupMap[backup.ID] = backup
}
// Convert map back to slice
newBackups := make([]PBSBackup, 0, len(backupMap))
for _, backup := range backupMap {
newBackups = append(newBackups, backup)
}
// Sort by backup time (newest first)
sort.Slice(newBackups, func(i, j int) bool {
return newBackups[i].BackupTime.After(newBackups[j].BackupTime)
})
s.PBSBackups = newBackups
s.LastUpdate = time.Now()
}