diff --git a/frontend-modern/src/api/charts.ts b/frontend-modern/src/api/charts.ts index 11f224b25e..62feb0cf9d 100644 --- a/frontend-modern/src/api/charts.ts +++ b/frontend-modern/src/api/charts.ts @@ -41,6 +41,7 @@ export interface ChartsResponse { storageData: Record; // Storage data keyed by ID dockerData?: Record; // Docker container data keyed by container ID dockerHostData?: Record; // Docker host data keyed by host ID + hostData?: Record; // Unified host agent data keyed by host ID guestTypes?: Record; // Maps guest ID to type timestamp: number; stats: ChartStats; diff --git a/frontend-modern/src/components/Hosts/HostsOverview.tsx b/frontend-modern/src/components/Hosts/HostsOverview.tsx index eb20e7ea36..4e8ced2d0a 100644 --- a/frontend-modern/src/components/Hosts/HostsOverview.tsx +++ b/frontend-modern/src/components/Hosts/HostsOverview.tsx @@ -23,6 +23,7 @@ import { useAlertsActivation } from '@/stores/alertsActivation'; import { HostMetadataAPI, type HostMetadata } from '@/api/hostMetadata'; import { logger } from '@/utils/logger'; +import { buildMetricKey } from '@/utils/metricsKeys'; import { isKioskMode, subscribeToKioskMode } from '@/utils/url'; import { HostDrawer } from './HostDrawer'; @@ -1237,6 +1238,7 @@ const HostRow: Component = (props) => { usage={cpuPercent()} loadAverage={props.host.loadAverage?.[0]} cores={props.isMobile() ? undefined : props.host.cpuCount} + resourceId={buildMetricKey('host', props.host.id)} /> @@ -1252,6 +1254,7 @@ const HostRow: Component = (props) => { balloon={props.host.memory?.balloon || 0} swapUsed={props.host.memory?.swapUsed || 0} swapTotal={props.host.memory?.swapTotal || 0} + resourceId={buildMetricKey('host', props.host.id)} /> diff --git a/frontend-modern/src/stores/metricsHistory.ts b/frontend-modern/src/stores/metricsHistory.ts index fd85c5d842..92011f7280 100644 --- a/frontend-modern/src/stores/metricsHistory.ts +++ b/frontend-modern/src/stores/metricsHistory.ts @@ -411,6 +411,16 @@ export async function seedFromBackend(range: TimeRange = '1h'): Promise { }); } + // Process unified host agents + if (response.hostData) { + for (const [id, chartData] of Object.entries(response.hostData)) { + const resourceKey = buildMetricKey('host', id); + processChartData(resourceKey, chartData as ChartData); + } + logger.debug('[MetricsHistory] Processed unified host agent data', { + count: Object.keys(response.hostData).length + }); + } hasSeededFromBackend = true; logger.info('[MetricsHistory] Seeded from backend', { seededCount, totalResources: metricsHistoryMap.size }); diff --git a/frontend-modern/src/stores/metricsViewMode.ts b/frontend-modern/src/stores/metricsViewMode.ts index 6a5b626615..737ef6a8b4 100644 --- a/frontend-modern/src/stores/metricsViewMode.ts +++ b/frontend-modern/src/stores/metricsViewMode.ts @@ -20,6 +20,8 @@ export const TIME_RANGE_OPTIONS: { value: TimeRange; label: string }[] = [ { value: '1h', label: '1h' }, { value: '4h', label: '4h' }, { value: '24h', label: '24h' }, + { value: '7d', label: '7d' }, + { value: '30d', label: '30d' }, ]; // Read initial view mode from localStorage @@ -44,7 +46,7 @@ const getInitialTimeRange = (): TimeRange => { try { const stored = localStorage.getItem(STORAGE_KEYS.METRICS_TIME_RANGE); - if (stored && ['5m', '15m', '30m', '1h', '4h', '12h', '24h', '7d'].includes(stored)) { + if (stored && ['5m', '15m', '30m', '1h', '4h', '12h', '24h', '7d', '30d'].includes(stored)) { return stored as TimeRange; } } catch (_err) { diff --git a/frontend-modern/src/utils/metricsKeys.ts b/frontend-modern/src/utils/metricsKeys.ts index 13b1529a88..50dd44d1df 100644 --- a/frontend-modern/src/utils/metricsKeys.ts +++ b/frontend-modern/src/utils/metricsKeys.ts @@ -5,7 +5,7 @@ * across different resource types. */ -export type MetricResourceKind = 'node' | 'vm' | 'container' | 'dockerHost' | 'dockerContainer'; +export type MetricResourceKind = 'node' | 'vm' | 'container' | 'host' | 'dockerHost' | 'dockerContainer'; /** * Build a namespaced metric key for a resource diff --git a/internal/api/router.go b/internal/api/router.go index 87aa2b369a..5be40d69b3 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -5328,12 +5328,62 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { } } + // Process unified host agents - get historical data + hostData := make(map[string]VMChartData) + for _, host := range state.Hosts { + if host.ID == "" { + continue + } + + if hostData[host.ID] == nil { + hostData[host.ID] = make(VMChartData) + } + + // Get historical metrics using the host: prefix key + metricKey := fmt.Sprintf("host:%s", host.ID) + metrics := monitor.GetGuestMetrics(metricKey, duration) + + // Convert metric points to API format + for metricType, points := range metrics { + hostData[host.ID][metricType] = make([]MetricPoint, len(points)) + for i, point := range points { + ts := point.Timestamp.Unix() * 1000 + if ts < oldestTimestamp { + oldestTimestamp = ts + } + hostData[host.ID][metricType][i] = MetricPoint{ + Timestamp: ts, + Value: point.Value, + } + } + } + + // If no historical data, add current value + if len(hostData[host.ID]["cpu"]) == 0 { + hostData[host.ID]["cpu"] = []MetricPoint{ + {Timestamp: currentTime, Value: host.CPUUsage}, + } + hostData[host.ID]["memory"] = []MetricPoint{ + {Timestamp: currentTime, Value: host.Memory.Usage}, + } + // Use first disk for host disk percentage + var diskPercent float64 + if len(host.Disks) > 0 { + diskPercent = host.Disks[0].Usage + } + hostData[host.ID]["disk"] = []MetricPoint{ + {Timestamp: currentTime, Value: diskPercent}, + } + } + } + response := ChartResponse{ ChartData: chartData, NodeData: nodeData, StorageData: storageData, DockerData: dockerData, DockerHostData: dockerHostData, + HostData: hostData, GuestTypes: guestTypes, Timestamp: currentTime, Stats: ChartStats{ @@ -5353,6 +5403,7 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { Int("nodes", len(nodeData)). Int("storage", len(storageData)). Int("dockerContainers", len(dockerData)). + Int("hosts", len(hostData)). Str("range", timeRange). Msg("Chart data response sent") } diff --git a/internal/api/types.go b/internal/api/types.go index 8c216489aa..f0763fa701 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -67,6 +67,7 @@ type ChartResponse struct { StorageData map[string]StorageChartData `json:"storageData"` DockerData map[string]VMChartData `json:"dockerData"` // Docker container metrics (keyed by container ID) DockerHostData map[string]VMChartData `json:"dockerHostData"` // Docker host metrics (keyed by host ID) + HostData map[string]VMChartData `json:"hostData"` // Unified host agent metrics (keyed by host ID) GuestTypes map[string]string `json:"guestTypes"` // Maps guest ID to type ("vm" or "container") Timestamp int64 `json:"timestamp"` Stats ChartStats `json:"stats"`