feat: Add sparkline support for unified host agents on hosts page

Backend:
- Add HostData field to ChartResponse struct in types.go
- Add host data processing in /api/charts endpoint using 'host:' prefix key
- Include hosts count in debug logging for chart responses

Frontend:
- Add 'host' to MetricResourceKind type in metricsKeys.ts
- Add hostData field to ChartsResponse interface in charts.ts
- Process hostData in seedFromBackend() in metricsHistory.ts
- Pass resourceId to EnhancedCPUBar and StackedMemoryBar in HostsOverview.tsx
- Add '7d' and '30d' to TIME_RANGE_OPTIONS in metricsViewMode.ts

This enables sparkline trend visualization for unified host agents,
consistent with Proxmox guests. Data accumulates over time at 30s intervals.
This commit is contained in:
rcourtman 2026-02-03 22:59:55 +00:00
parent a9d7713313
commit 6059759958
7 changed files with 70 additions and 2 deletions

View file

@ -41,6 +41,7 @@ export interface ChartsResponse {
storageData: Record<string, ChartData>; // Storage data keyed by ID
dockerData?: Record<string, ChartData>; // Docker container data keyed by container ID
dockerHostData?: Record<string, ChartData>; // Docker host data keyed by host ID
hostData?: Record<string, ChartData>; // Unified host agent data keyed by host ID
guestTypes?: Record<string, 'vm' | 'container'>; // Maps guest ID to type
timestamp: number;
stats: ChartStats;

View file

@ -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<HostRowProps> = (props) => {
usage={cpuPercent()}
loadAverage={props.host.loadAverage?.[0]}
cores={props.isMobile() ? undefined : props.host.cpuCount}
resourceId={buildMetricKey('host', props.host.id)}
/>
</Show>
</td>
@ -1252,6 +1254,7 @@ const HostRow: Component<HostRowProps> = (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)}
/>
</Show>
</td>

View file

@ -411,6 +411,16 @@ export async function seedFromBackend(range: TimeRange = '1h'): Promise<void> {
});
}
// 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 });

View file

@ -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) {

View file

@ -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

View file

@ -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")
}

View file

@ -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"`