Files
pulse/internal/models/converters.go
T
rcourtman 118574e491 fix: expose linkedHostAgentId and commandsEnabled to frontend
Related to #952 and #971

Both issues were caused by the backend not sending required fields to the
frontend in the ToFrontend() converters:

Issue #971 (Agent required badge):
- NodeFrontend was missing LinkedHostAgentId field
- Frontend couldn't identify linked host agents, so it fell back to showing
  'Agent required' instead of 'Via agent'

Issue #952 (AI Commands toggle stuck):
- HostFrontend was missing CommandsEnabled field
- Frontend couldn't see the actual commandsEnabled state from the backend,
  causing the optimistic UI to never receive confirmation that the state
  had actually changed

Also added IsLegacy and LinkedNodeId to HostFrontend for completeness.
2026-01-02 20:04:20 +00:00

1061 lines
28 KiB
Go

package models
import (
"strings"
"time"
)
// ToFrontend converts a State to StateFrontend
func (s *State) ToFrontend() StateFrontend {
return s.GetSnapshot().ToFrontend()
}
// ToFrontend converts a Node to NodeFrontend
func (n Node) ToFrontend() NodeFrontend {
nf := NodeFrontend{
ID: n.ID,
Node: n.Name,
Name: n.Name,
DisplayName: n.DisplayName,
Instance: n.Instance,
Host: n.Host,
GuestURL: n.GuestURL,
Status: n.Status,
Type: n.Type,
CPU: n.CPU,
Mem: n.Memory.Used,
MaxMem: n.Memory.Total,
MaxDisk: n.Disk.Total,
Uptime: n.Uptime,
LoadAverage: n.LoadAverage,
KernelVersion: n.KernelVersion,
PVEVersion: n.PVEVersion,
CPUInfo: n.CPUInfo,
LastSeen: n.LastSeen.Unix() * 1000,
ConnectionHealth: n.ConnectionHealth,
IsClusterMember: n.IsClusterMember,
ClusterName: n.ClusterName,
TemperatureMonitoringEnabled: n.TemperatureMonitoringEnabled,
LinkedHostAgentId: n.LinkedHostAgentID,
}
// Include full Memory object if it has data
if n.Memory.Total > 0 {
nf.Memory = &n.Memory
}
// Include full Disk object if it has data
if n.Disk.Total > 0 {
nf.Disk = &n.Disk
}
// Include temperature data if available
if n.Temperature != nil && n.Temperature.Available {
nf.Temperature = n.Temperature
}
if nf.DisplayName == "" {
nf.DisplayName = nf.Name
}
return nf
}
// ToFrontend converts a VM to VMFrontend
func (v VM) ToFrontend() VMFrontend {
vm := VMFrontend{
ID: v.ID,
VMID: v.VMID,
Name: v.Name,
Node: v.Node,
Instance: v.Instance,
Status: v.Status,
Type: v.Type,
CPU: v.CPU,
CPUs: v.CPUs,
Mem: v.Memory.Used,
MaxMem: v.Memory.Total,
NetIn: zeroIfNegative(v.NetworkIn),
NetOut: zeroIfNegative(v.NetworkOut),
DiskRead: zeroIfNegative(v.DiskRead),
DiskWrite: zeroIfNegative(v.DiskWrite),
Uptime: v.Uptime,
Template: v.Template,
Lock: v.Lock,
LastSeen: v.LastSeen.Unix() * 1000,
DiskStatusReason: v.DiskStatusReason,
}
// Convert tags array to string
if len(v.Tags) > 0 {
vm.Tags = strings.Join(v.Tags, ",")
}
// Convert last backup time if not zero
if !v.LastBackup.IsZero() {
vm.LastBackup = v.LastBackup.Unix() * 1000
}
// Include full Memory object if it has data
if v.Memory.Total > 0 {
vm.Memory = &v.Memory
}
// Include full Disk object if it has data
if v.Disk.Total > 0 {
vm.DiskObj = &v.Disk
}
// Include individual disks array if available
if len(v.Disks) > 0 {
vm.Disks = v.Disks
}
if len(v.IPAddresses) > 0 {
vm.IPAddresses = append([]string(nil), v.IPAddresses...)
}
if v.OSName != "" {
vm.OSName = v.OSName
}
if v.OSVersion != "" {
vm.OSVersion = v.OSVersion
}
if v.AgentVersion != "" {
vm.AgentVersion = v.AgentVersion
}
if len(v.NetworkInterfaces) > 0 {
vm.NetworkInterfaces = make([]GuestNetworkInterface, len(v.NetworkInterfaces))
copy(vm.NetworkInterfaces, v.NetworkInterfaces)
}
return vm
}
// ToFrontend converts a Container to ContainerFrontend
func (c Container) ToFrontend() ContainerFrontend {
ct := ContainerFrontend{
ID: c.ID,
VMID: c.VMID,
Name: c.Name,
Node: c.Node,
Instance: c.Instance,
Status: c.Status,
Type: c.Type,
CPU: c.CPU,
CPUs: c.CPUs,
Mem: c.Memory.Used,
MaxMem: c.Memory.Total,
NetIn: zeroIfNegative(c.NetworkIn),
NetOut: zeroIfNegative(c.NetworkOut),
DiskRead: zeroIfNegative(c.DiskRead),
DiskWrite: zeroIfNegative(c.DiskWrite),
Uptime: c.Uptime,
Template: c.Template,
Lock: c.Lock,
LastSeen: c.LastSeen.Unix() * 1000,
}
// OCI containers are classified separately from traditional LXC containers in the UI.
// Keep the frontend payload stable even if some internal sources only persist Type.
isOCI := c.IsOCI || strings.EqualFold(strings.TrimSpace(c.Type), "oci")
if isOCI {
ct.IsOCI = true
ct.Type = "oci"
}
// Preserve template metadata (LXC template or OCI image reference).
if c.OSTemplate != "" {
ct.OSTemplate = c.OSTemplate
}
// Convert tags array to string
if len(c.Tags) > 0 {
ct.Tags = strings.Join(c.Tags, ",")
}
// Convert last backup time if not zero
if !c.LastBackup.IsZero() {
ct.LastBackup = c.LastBackup.Unix() * 1000
}
// Include full Memory object if it has data
if c.Memory.Total > 0 {
ct.Memory = &c.Memory
}
// Include full Disk object if it has data
if c.Disk.Total > 0 {
ct.DiskObj = &c.Disk
}
// Include individual disks array if available
if len(c.Disks) > 0 {
ct.Disks = c.Disks
}
if len(c.IPAddresses) > 0 {
ct.IPAddresses = append([]string(nil), c.IPAddresses...)
}
if len(c.NetworkInterfaces) > 0 {
ct.NetworkInterfaces = make([]GuestNetworkInterface, len(c.NetworkInterfaces))
copy(ct.NetworkInterfaces, c.NetworkInterfaces)
}
if c.OSName != "" {
ct.OSName = c.OSName
}
return ct
}
// ToFrontend converts a DockerHost to DockerHostFrontend
func (d DockerHost) ToFrontend() DockerHostFrontend {
h := DockerHostFrontend{
ID: d.ID,
AgentID: d.AgentID,
Hostname: d.Hostname,
DisplayName: d.DisplayName,
CustomDisplayName: d.CustomDisplayName,
MachineID: d.MachineID,
OS: d.OS,
KernelVersion: d.KernelVersion,
Architecture: d.Architecture,
Runtime: d.Runtime,
RuntimeVersion: d.RuntimeVersion,
DockerVersion: d.DockerVersion,
CPUs: d.CPUs,
TotalMemoryBytes: d.TotalMemoryBytes,
UptimeSeconds: d.UptimeSeconds,
CPUUsagePercent: d.CPUUsage,
Status: d.Status,
LastSeen: d.LastSeen.Unix() * 1000,
IntervalSeconds: d.IntervalSeconds,
AgentVersion: d.AgentVersion,
Containers: make([]DockerContainerFrontend, len(d.Containers)),
}
if h.DisplayName == "" {
h.DisplayName = h.Hostname
}
h.PendingUninstall = d.PendingUninstall
if d.TokenID != "" {
h.TokenID = d.TokenID
h.TokenName = d.TokenName
h.TokenHint = d.TokenHint
if d.TokenLastUsedAt != nil && !d.TokenLastUsedAt.IsZero() {
ts := d.TokenLastUsedAt.Unix() * 1000
h.TokenLastUsedAt = &ts
}
}
for i, ct := range d.Containers {
h.Containers[i] = ct.ToFrontend()
}
if len(d.Services) > 0 {
h.Services = make([]DockerServiceFrontend, len(d.Services))
for i, svc := range d.Services {
h.Services[i] = svc.ToFrontend()
}
}
if len(d.Tasks) > 0 {
h.Tasks = make([]DockerTaskFrontend, len(d.Tasks))
for i, task := range d.Tasks {
h.Tasks[i] = task.ToFrontend()
}
}
if d.Swarm != nil {
sw := d.Swarm.ToFrontend()
h.Swarm = &sw
}
if len(d.LoadAverage) > 0 {
h.LoadAverage = append([]float64(nil), d.LoadAverage...)
}
if (d.Memory != Memory{}) {
mem := d.Memory
h.Memory = &mem
}
if len(d.Disks) > 0 {
h.Disks = append([]Disk(nil), d.Disks...)
}
if len(d.NetworkInterfaces) > 0 {
h.NetworkInterfaces = make([]HostNetworkInterface, len(d.NetworkInterfaces))
copy(h.NetworkInterfaces, d.NetworkInterfaces)
}
if d.Command != nil {
h.Command = toDockerHostCommandFrontend(*d.Command)
}
return h
}
// ToFrontend converts a RemovedDockerHost to its frontend representation.
func (r RemovedDockerHost) ToFrontend() RemovedDockerHostFrontend {
return RemovedDockerHostFrontend{
ID: r.ID,
Hostname: r.Hostname,
DisplayName: r.DisplayName,
RemovedAt: r.RemovedAt.Unix() * 1000,
}
}
// ToFrontend converts a KubernetesCluster to its frontend representation.
func (c KubernetesCluster) ToFrontend() KubernetesClusterFrontend {
cluster := KubernetesClusterFrontend{
ID: c.ID,
AgentID: c.AgentID,
Name: c.Name,
DisplayName: c.DisplayName,
CustomDisplayName: c.CustomDisplayName,
Server: c.Server,
Context: c.Context,
Version: c.Version,
Status: c.Status,
LastSeen: c.LastSeen.Unix() * 1000,
IntervalSeconds: c.IntervalSeconds,
AgentVersion: c.AgentVersion,
TokenID: c.TokenID,
TokenName: c.TokenName,
TokenHint: c.TokenHint,
Hidden: c.Hidden,
PendingUninstall: c.PendingUninstall,
}
if c.TokenLastUsedAt != nil {
ts := c.TokenLastUsedAt.Unix() * 1000
cluster.TokenLastUsedAt = &ts
}
if len(c.Nodes) > 0 {
cluster.Nodes = make([]KubernetesNodeFrontend, len(c.Nodes))
for i, n := range c.Nodes {
cluster.Nodes[i] = KubernetesNodeFrontend{
UID: n.UID,
Name: n.Name,
Ready: n.Ready,
Unschedulable: n.Unschedulable,
KubeletVersion: n.KubeletVersion,
ContainerRuntimeVersion: n.ContainerRuntimeVersion,
OSImage: n.OSImage,
KernelVersion: n.KernelVersion,
Architecture: n.Architecture,
CapacityCPU: n.CapacityCPU,
CapacityMemoryBytes: n.CapacityMemoryBytes,
CapacityPods: n.CapacityPods,
AllocCPU: n.AllocCPU,
AllocMemoryBytes: n.AllocMemoryBytes,
AllocPods: n.AllocPods,
Roles: append([]string(nil), n.Roles...),
}
}
}
if len(c.Pods) > 0 {
cluster.Pods = make([]KubernetesPodFrontend, len(c.Pods))
for i, p := range c.Pods {
labels := make(map[string]string, len(p.Labels))
for k, v := range p.Labels {
labels[k] = v
}
containers := make([]KubernetesPodContainerFrontend, 0, len(p.Containers))
for _, cn := range p.Containers {
containers = append(containers, KubernetesPodContainerFrontend{
Name: cn.Name,
Image: cn.Image,
Ready: cn.Ready,
RestartCount: cn.RestartCount,
State: cn.State,
Reason: cn.Reason,
Message: cn.Message,
})
}
var startTime *int64
if p.StartTime != nil {
ts := p.StartTime.Unix() * 1000
startTime = &ts
}
cluster.Pods[i] = KubernetesPodFrontend{
UID: p.UID,
Name: p.Name,
Namespace: p.Namespace,
NodeName: p.NodeName,
Phase: p.Phase,
Reason: p.Reason,
Message: p.Message,
QoSClass: p.QoSClass,
CreatedAt: timeToUnixMillis(p.CreatedAt),
StartTime: startTime,
Restarts: p.Restarts,
Labels: labels,
OwnerKind: p.OwnerKind,
OwnerName: p.OwnerName,
Containers: containers,
}
}
}
if len(c.Deployments) > 0 {
cluster.Deployments = make([]KubernetesDeploymentFrontend, len(c.Deployments))
for i, d := range c.Deployments {
labels := make(map[string]string, len(d.Labels))
for k, v := range d.Labels {
labels[k] = v
}
cluster.Deployments[i] = KubernetesDeploymentFrontend{
UID: d.UID,
Name: d.Name,
Namespace: d.Namespace,
DesiredReplicas: d.DesiredReplicas,
UpdatedReplicas: d.UpdatedReplicas,
ReadyReplicas: d.ReadyReplicas,
AvailableReplicas: d.AvailableReplicas,
Labels: labels,
}
}
}
if cluster.DisplayName == "" && cluster.Name != "" {
cluster.DisplayName = cluster.Name
}
if cluster.DisplayName == "" {
cluster.DisplayName = cluster.ID
}
return cluster
}
func timeToUnixMillis(t time.Time) int64 {
if t.IsZero() {
return 0
}
return t.Unix() * 1000
}
// ToFrontend converts a RemovedKubernetesCluster to its frontend representation.
func (r RemovedKubernetesCluster) ToFrontend() RemovedKubernetesClusterFrontend {
return RemovedKubernetesClusterFrontend{
ID: r.ID,
Name: r.Name,
DisplayName: r.DisplayName,
RemovedAt: r.RemovedAt.Unix() * 1000,
}
}
// ToFrontend converts a Host to HostFrontend.
func (h Host) ToFrontend() HostFrontend {
host := HostFrontend{
ID: h.ID,
Hostname: h.Hostname,
DisplayName: h.DisplayName,
Platform: h.Platform,
OSName: h.OSName,
OSVersion: h.OSVersion,
KernelVersion: h.KernelVersion,
Architecture: h.Architecture,
CPUCount: h.CPUCount,
CPUUsage: h.CPUUsage,
Status: h.Status,
UptimeSeconds: h.UptimeSeconds,
IntervalSeconds: h.IntervalSeconds,
AgentVersion: h.AgentVersion,
TokenID: h.TokenID,
TokenName: h.TokenName,
TokenHint: h.TokenHint,
Tags: append([]string(nil), h.Tags...),
LastSeen: h.LastSeen.Unix() * 1000,
CommandsEnabled: h.CommandsEnabled,
IsLegacy: h.IsLegacy,
LinkedNodeId: h.LinkedNodeID,
}
// Fall back to Hostname if DisplayName is empty
if host.DisplayName == "" && h.Hostname != "" {
host.DisplayName = h.Hostname
}
if len(h.LoadAverage) > 0 {
host.LoadAverage = append([]float64(nil), h.LoadAverage...)
}
if (h.Memory != Memory{}) {
mem := h.Memory
host.Memory = &mem
}
if len(h.Disks) > 0 {
host.Disks = append([]Disk(nil), h.Disks...)
}
if len(h.DiskIO) > 0 {
host.DiskIO = append([]DiskIO(nil), h.DiskIO...)
}
if len(h.NetworkInterfaces) > 0 {
host.NetworkInterfaces = make([]HostNetworkInterface, len(h.NetworkInterfaces))
copy(host.NetworkInterfaces, h.NetworkInterfaces)
}
if s := hostSensorSummaryToFrontend(h.Sensors); s != nil {
host.Sensors = s
}
if h.TokenLastUsedAt != nil && !h.TokenLastUsedAt.IsZero() {
ts := h.TokenLastUsedAt.Unix() * 1000
host.TokenLastUsedAt = &ts
}
return host
}
// ToFrontend converts a DockerContainer to DockerContainerFrontend
func (c DockerContainer) ToFrontend() DockerContainerFrontend {
container := DockerContainerFrontend{
ID: c.ID,
Name: c.Name,
Image: c.Image,
State: c.State,
Status: c.Status,
Health: c.Health,
CPUPercent: c.CPUPercent,
MemoryUsage: c.MemoryUsage,
MemoryLimit: c.MemoryLimit,
MemoryPercent: c.MemoryPercent,
UptimeSeconds: c.UptimeSeconds,
RestartCount: c.RestartCount,
ExitCode: c.ExitCode,
CreatedAt: c.CreatedAt.Unix() * 1000,
Labels: c.Labels,
WritableLayerBytes: c.WritableLayerBytes,
RootFilesystemBytes: c.RootFilesystemBytes,
}
if c.StartedAt != nil {
ms := c.StartedAt.Unix() * 1000
container.StartedAt = &ms
}
if c.FinishedAt != nil {
ms := c.FinishedAt.Unix() * 1000
container.FinishedAt = &ms
}
if len(c.Ports) > 0 {
ports := make([]DockerContainerPortFrontend, len(c.Ports))
for i, port := range c.Ports {
ports[i] = DockerContainerPortFrontend(port)
}
container.Ports = ports
}
if len(c.Networks) > 0 {
networks := make([]DockerContainerNetworkFrontend, len(c.Networks))
for i, net := range c.Networks {
networks[i] = DockerContainerNetworkFrontend(net)
}
container.Networks = networks
}
if c.BlockIO != nil {
container.BlockIO = &DockerContainerBlockIOFrontend{
ReadBytes: c.BlockIO.ReadBytes,
WriteBytes: c.BlockIO.WriteBytes,
ReadRateBytesPerSecond: c.BlockIO.ReadRateBytesPerSecond,
WriteRateBytesPerSecond: c.BlockIO.WriteRateBytesPerSecond,
}
}
if len(c.Mounts) > 0 {
mounts := make([]DockerContainerMountFrontend, len(c.Mounts))
for i, mount := range c.Mounts {
mounts[i] = DockerContainerMountFrontend(mount)
}
container.Mounts = mounts
}
if c.Podman != nil {
container.Podman = &DockerPodmanContainerFrontend{
PodName: c.Podman.PodName,
PodID: c.Podman.PodID,
Infra: c.Podman.Infra,
ComposeProject: c.Podman.ComposeProject,
ComposeService: c.Podman.ComposeService,
ComposeWorkdir: c.Podman.ComposeWorkdir,
ComposeConfigHash: c.Podman.ComposeConfigHash,
AutoUpdatePolicy: c.Podman.AutoUpdatePolicy,
AutoUpdateRestart: c.Podman.AutoUpdateRestart,
UserNamespace: c.Podman.UserNamespace,
}
}
if c.UpdateStatus != nil {
container.UpdateStatus = &DockerContainerUpdateStatusFrontend{
UpdateAvailable: c.UpdateStatus.UpdateAvailable,
CurrentDigest: c.UpdateStatus.CurrentDigest,
LatestDigest: c.UpdateStatus.LatestDigest,
LastChecked: timeToUnixMillis(c.UpdateStatus.LastChecked),
Error: c.UpdateStatus.Error,
}
}
return container
}
// ToFrontend converts a DockerService to DockerServiceFrontend.
func (s DockerService) ToFrontend() DockerServiceFrontend {
service := DockerServiceFrontend{
ID: s.ID,
Name: s.Name,
Stack: s.Stack,
Image: s.Image,
Mode: s.Mode,
DesiredTasks: s.DesiredTasks,
RunningTasks: s.RunningTasks,
CompletedTasks: s.CompletedTasks,
Labels: nil,
}
if len(s.Labels) > 0 {
service.Labels = make(map[string]string, len(s.Labels))
for k, v := range s.Labels {
service.Labels[k] = v
}
}
if len(s.EndpointPorts) > 0 {
service.EndpointPorts = make([]DockerServicePortFrontend, len(s.EndpointPorts))
for i, port := range s.EndpointPorts {
service.EndpointPorts[i] = port.ToFrontend()
}
}
if s.UpdateStatus != nil {
update := s.UpdateStatus.ToFrontend()
service.UpdateStatus = &update
}
if s.CreatedAt != nil && !s.CreatedAt.IsZero() {
ts := s.CreatedAt.Unix() * 1000
service.CreatedAt = &ts
}
if s.UpdatedAt != nil && !s.UpdatedAt.IsZero() {
ts := s.UpdatedAt.Unix() * 1000
service.UpdatedAt = &ts
}
return service
}
// ToFrontend converts a DockerServicePort to DockerServicePortFrontend.
func (p DockerServicePort) ToFrontend() DockerServicePortFrontend {
return DockerServicePortFrontend(p)
}
// ToFrontend converts a DockerServiceUpdate to DockerServiceUpdateFrontend.
func (u DockerServiceUpdate) ToFrontend() DockerServiceUpdateFrontend {
update := DockerServiceUpdateFrontend{
State: u.State,
Message: u.Message,
}
if u.CompletedAt != nil && !u.CompletedAt.IsZero() {
ts := u.CompletedAt.Unix() * 1000
update.CompletedAt = &ts
}
return update
}
// ToFrontend converts a DockerTask to DockerTaskFrontend.
func (t DockerTask) ToFrontend() DockerTaskFrontend {
task := DockerTaskFrontend{
ID: t.ID,
ServiceID: t.ServiceID,
ServiceName: t.ServiceName,
Slot: t.Slot,
NodeID: t.NodeID,
NodeName: t.NodeName,
DesiredState: t.DesiredState,
CurrentState: t.CurrentState,
Error: t.Error,
Message: t.Message,
ContainerID: t.ContainerID,
ContainerName: t.ContainerName,
}
if !t.CreatedAt.IsZero() {
ts := t.CreatedAt.Unix() * 1000
task.CreatedAt = &ts
}
if t.UpdatedAt != nil && !t.UpdatedAt.IsZero() {
ts := t.UpdatedAt.Unix() * 1000
task.UpdatedAt = &ts
}
if t.StartedAt != nil && !t.StartedAt.IsZero() {
ts := t.StartedAt.Unix() * 1000
task.StartedAt = &ts
}
if t.CompletedAt != nil && !t.CompletedAt.IsZero() {
ts := t.CompletedAt.Unix() * 1000
task.CompletedAt = &ts
}
return task
}
// ToFrontend converts DockerSwarmInfo to DockerSwarmFrontend.
func (s DockerSwarmInfo) ToFrontend() DockerSwarmFrontend {
return DockerSwarmFrontend(s)
}
func hostSensorSummaryToFrontend(src HostSensorSummary) *HostSensorSummaryFrontend {
if len(src.TemperatureCelsius) == 0 && len(src.FanRPM) == 0 && len(src.Additional) == 0 && len(src.SMART) == 0 {
return nil
}
dest := &HostSensorSummaryFrontend{}
if len(src.TemperatureCelsius) > 0 {
dest.TemperatureCelsius = copyStringFloatMap(src.TemperatureCelsius)
}
if len(src.FanRPM) > 0 {
dest.FanRPM = copyStringFloatMap(src.FanRPM)
}
if len(src.Additional) > 0 {
dest.Additional = copyStringFloatMap(src.Additional)
}
if len(src.SMART) > 0 {
dest.SMART = make([]HostDiskSMARTFrontend, len(src.SMART))
for i, disk := range src.SMART {
dest.SMART[i] = HostDiskSMARTFrontend{
Device: disk.Device,
Model: disk.Model,
Serial: disk.Serial,
WWN: disk.WWN,
Type: disk.Type,
Temperature: disk.Temperature,
Health: disk.Health,
Standby: disk.Standby,
}
}
}
return dest
}
func copyStringFloatMap(src map[string]float64) map[string]float64 {
if len(src) == 0 {
return nil
}
dest := make(map[string]float64, len(src))
for k, v := range src {
dest[k] = v
}
return dest
}
func toDockerHostCommandFrontend(cmd DockerHostCommandStatus) *DockerHostCommandFrontend {
result := &DockerHostCommandFrontend{
ID: cmd.ID,
Type: cmd.Type,
Status: cmd.Status,
Message: cmd.Message,
CreatedAt: cmd.CreatedAt.Unix() * 1000,
UpdatedAt: cmd.UpdatedAt.Unix() * 1000,
}
if cmd.DispatchedAt != nil {
ms := cmd.DispatchedAt.Unix() * 1000
result.DispatchedAt = &ms
}
if cmd.AcknowledgedAt != nil {
ms := cmd.AcknowledgedAt.Unix() * 1000
result.AcknowledgedAt = &ms
}
if cmd.CompletedAt != nil {
ms := cmd.CompletedAt.Unix() * 1000
result.CompletedAt = &ms
}
if cmd.FailedAt != nil {
ms := cmd.FailedAt.Unix() * 1000
result.FailedAt = &ms
}
if cmd.FailureReason != "" {
result.FailureReason = cmd.FailureReason
}
if cmd.ExpiresAt != nil {
ms := cmd.ExpiresAt.Unix() * 1000
result.ExpiresAt = &ms
}
return result
}
// ToFrontend converts Storage to StorageFrontend
func (s Storage) ToFrontend() StorageFrontend {
return StorageFrontend{
ID: s.ID,
Storage: s.Name,
Name: s.Name,
Node: s.Node,
Instance: s.Instance,
Nodes: s.Nodes,
NodeIDs: s.NodeIDs,
NodeCount: s.NodeCount,
Type: s.Type,
Status: s.Status,
Total: s.Total,
Used: s.Used,
Avail: s.Free,
Free: s.Free,
Usage: s.Usage,
Content: s.Content,
Shared: s.Shared,
Enabled: s.Enabled,
Active: s.Active,
}
}
// ToFrontend converts a CephCluster to CephClusterFrontend
func (c CephCluster) ToFrontend() CephClusterFrontend {
frontend := CephClusterFrontend{
ID: c.ID,
Instance: c.Instance,
Name: c.Name,
FSID: c.FSID,
Health: c.Health,
HealthMessage: c.HealthMessage,
TotalBytes: c.TotalBytes,
UsedBytes: c.UsedBytes,
AvailableBytes: c.AvailableBytes,
UsagePercent: c.UsagePercent,
NumMons: c.NumMons,
NumMgrs: c.NumMgrs,
NumOSDs: c.NumOSDs,
NumOSDsUp: c.NumOSDsUp,
NumOSDsIn: c.NumOSDsIn,
NumPGs: c.NumPGs,
LastUpdated: c.LastUpdated.Unix() * 1000,
}
if len(c.Pools) > 0 {
frontend.Pools = append([]CephPool(nil), c.Pools...)
}
if len(c.Services) > 0 {
frontend.Services = append([]CephServiceStatus(nil), c.Services...)
}
return frontend
}
// ToFrontend converts a replication job to a frontend representation.
func (r ReplicationJob) ToFrontend() ReplicationJobFrontend {
frontend := ReplicationJobFrontend{
ID: r.ID,
Instance: r.Instance,
JobID: r.JobID,
JobNumber: r.JobNumber,
Guest: r.Guest,
GuestID: r.GuestID,
GuestName: r.GuestName,
GuestType: r.GuestType,
GuestNode: r.GuestNode,
SourceNode: r.SourceNode,
SourceStorage: r.SourceStorage,
TargetNode: r.TargetNode,
TargetStorage: r.TargetStorage,
Schedule: r.Schedule,
Type: r.Type,
Enabled: r.Enabled,
State: r.State,
Status: r.Status,
LastSyncStatus: r.LastSyncStatus,
LastSyncUnix: r.LastSyncUnix,
LastSyncDurationSeconds: r.LastSyncDurationSeconds,
LastSyncDurationHuman: r.LastSyncDurationHuman,
NextSyncUnix: r.NextSyncUnix,
DurationSeconds: r.DurationSeconds,
DurationHuman: r.DurationHuman,
FailCount: r.FailCount,
Error: r.Error,
Comment: r.Comment,
RemoveJob: r.RemoveJob,
RateLimitMbps: r.RateLimitMbps,
}
if r.LastSyncTime != nil {
frontend.LastSyncTime = r.LastSyncTime.UnixMilli()
}
if r.NextSyncTime != nil {
frontend.NextSyncTime = r.NextSyncTime.UnixMilli()
}
polledAt := r.LastPolled
if polledAt.IsZero() {
polledAt = time.Now()
}
frontend.PolledAt = polledAt.UnixMilli()
return frontend
}
// zeroIfNegative returns 0 for negative values (used for I/O metrics)
func zeroIfNegative(val int64) int64 {
if val < 0 {
return 0
}
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
}