diff --git a/.gitignore b/.gitignore index 0ccfbc7b4..f899eb150 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,12 @@ screenshots/ # Master plan documents (local only) PULSE_V4_ISSUES_MASTER_PLAN.md FIX_SUMMARY_*.md + +# Development documentation +TYPING_*.md +test-config.json + +# Local test scripts +scripts/test-*.sh +scripts/run-tests.sh +scripts/TEST_*.md diff --git a/frontend-modern/src/api/notifications.ts b/frontend-modern/src/api/notifications.ts index 756f0f460..da9c5eea5 100644 --- a/frontend-modern/src/api/notifications.ts +++ b/frontend-modern/src/api/notifications.ts @@ -58,7 +58,7 @@ export interface Webhook { export interface NotificationTestRequest { type: 'email' | 'webhook'; - config?: any; // Backend expects different format than frontend types + config?: Record; // Backend expects different format than frontend types webhookId?: string; } @@ -141,7 +141,7 @@ export class NotificationsAPI { // Testing static async testNotification(request: NotificationTestRequest): Promise<{ success: boolean; message?: string }> { - const body: { method: string; config?: any } = { method: request.type }; + const body: { method: string; config?: Record } = { method: request.type }; // Include config if provided for testing without saving if (request.config) { diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 3051a4788..65152f158 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -279,8 +279,12 @@ const Settings: Component = () => { const existingMap = new Map(prev.map(s => [`${s.ip}:${s.port}`, s])); // Add/update the new servers - data.servers.forEach((server: any) => { - existingMap.set(`${server.ip}:${server.port}`, server); + data.servers.forEach((server) => { + const discoveredServer: DiscoveredServer = { + ...server, + type: server.type as 'pbs' | 'pve' + }; + existingMap.set(`${server.ip}:${server.port}`, discoveredServer); }); // Convert back to array diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index 150766715..1723d197e 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -303,7 +303,7 @@ const Storage: Component = () => { {/* Helpful hint for no PVE nodes but still show content */} - n.type === 'pve').length === 0 && sortedStorage().length === 0}> + n.type === 'pve').length === 0 && sortedStorage().length === 0}>
diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 90e49deb4..0fd669623 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -1493,7 +1493,7 @@ function DestinationsTab(props: DestinationsTabProps) { const config = props.emailConfig(); await NotificationsAPI.testNotification({ type: 'email', - config: config // Send current form data, not saved config + config: { ...config } as Record // Send current form data, not saved config }); showSuccess('Test email sent successfully!', 'Check your inbox.'); } catch (err) { diff --git a/frontend-modern/src/stores/events.ts b/frontend-modern/src/stores/events.ts index 44aafbbc0..5e8015880 100644 --- a/frontend-modern/src/stores/events.ts +++ b/frontend-modern/src/stores/events.ts @@ -3,25 +3,60 @@ // Event types export type EventType = 'node_auto_registered' | 'refresh_nodes' | 'discovery_updated'; -// Event handlers -type EventHandler = (data?: any) => void; +// Event data types +export interface NodeAutoRegisteredData { + type: string; + host: string; + name: string; + tokenId: string; + hasToken: boolean; + verifySSL?: boolean; + status?: string; + nodeId?: string; + nodeName?: string; +} + +export interface DiscoveryUpdatedData { + servers: Array<{ + ip: string; + port: number; + type: string; + version: string; + hostname?: string; + release?: string; + }>; + errors?: string[]; + timestamp?: number; + immediate?: boolean; + discoveredNodes?: number; +} + +// Map event types to their data types +export type EventDataMap = { + 'node_auto_registered': NodeAutoRegisteredData; + 'refresh_nodes': void; + 'discovery_updated': DiscoveryUpdatedData; +} + +// Generic event handler +type EventHandler = (data?: T) => void; class EventBus { - private handlers: Map> = new Map(); + private handlers: Map>> = new Map(); - on(event: EventType, handler: EventHandler) { + on(event: T, handler: EventHandler) { if (!this.handlers.has(event)) { this.handlers.set(event, new Set()); } - this.handlers.get(event)!.add(handler); + this.handlers.get(event)!.add(handler as EventHandler); // Return unsubscribe function return () => { - this.handlers.get(event)?.delete(handler); + this.handlers.get(event)?.delete(handler as EventHandler); }; } - emit(event: EventType, data?: any) { + emit(event: T, data?: EventDataMap[T]) { const handlers = this.handlers.get(event); if (handlers) { handlers.forEach(handler => handler(data)); diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts index b270e6c45..3c56b9a5f 100644 --- a/frontend-modern/src/types/api.ts +++ b/frontend-modern/src/types/api.ts @@ -331,7 +331,12 @@ export type WSMessage = | { type: 'welcome'; data?: unknown } | { type: 'alert'; data: Alert } | { type: 'alertResolved'; data: { alertId: string } } - | { type: 'update:progress'; data: any } + | { type: 'update:progress'; data: { + phase: string; + progress: number; + message: string; + } + } | { type: 'node_auto_registered'; data: { type: string; host: string; diff --git a/frontend-modern/src/utils/errorHandler.ts b/frontend-modern/src/utils/errorHandler.ts index 9d79c2181..96192bc43 100644 --- a/frontend-modern/src/utils/errorHandler.ts +++ b/frontend-modern/src/utils/errorHandler.ts @@ -3,7 +3,7 @@ import { logger } from './logger'; export interface ErrorContext { component?: string; action?: string; - data?: any; + data?: unknown; } export class AppError extends Error { diff --git a/internal/api/diagnostics.go b/internal/api/diagnostics.go index e7ee9597c..301d22f0c 100644 --- a/internal/api/diagnostics.go +++ b/internal/api/diagnostics.go @@ -26,26 +26,41 @@ type DiagnosticsInfo struct { // NodeDiagnostic contains diagnostic info for a Proxmox node type NodeDiagnostic struct { - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Type string `json:"type"` - AuthMethod string `json:"authMethod"` - Connected bool `json:"connected"` - Error string `json:"error,omitempty"` - Details map[string]interface{} `json:"details,omitempty"` - LastPoll string `json:"lastPoll,omitempty"` - ClusterInfo map[string]interface{} `json:"clusterInfo,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Host string `json:"host"` + Type string `json:"type"` + AuthMethod string `json:"authMethod"` + Connected bool `json:"connected"` + Error string `json:"error,omitempty"` + Details *NodeDetails `json:"details,omitempty"` + LastPoll string `json:"lastPoll,omitempty"` + ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty"` +} + +// NodeDetails contains node-specific details +type NodeDetails struct { + NodeCount int `json:"node_count,omitempty"` +} + +// ClusterInfo contains cluster information +type ClusterInfo struct { + Nodes int `json:"nodes"` } // PBSDiagnostic contains diagnostic info for a PBS instance type PBSDiagnostic struct { - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Connected bool `json:"connected"` - Error string `json:"error,omitempty"` - Details map[string]interface{} `json:"details,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Host string `json:"host"` + Connected bool `json:"connected"` + Error string `json:"error,omitempty"` + Details *PBSDetails `json:"details,omitempty"` +} + +// PBSDetails contains PBS-specific details +type PBSDetails struct { + Version string `json:"version,omitempty"` } // SystemDiagnostic contains system-level diagnostic info @@ -131,14 +146,14 @@ func (r *Router) handleDiagnostics(w http.ResponseWriter, req *http.Request) { nodeDiag.Error = "Connection established but cluster status failed: " + err.Error() } else { nodeDiag.Connected = true - nodeDiag.ClusterInfo = map[string]interface{}{ - "nodes": len(clusterStatus), + nodeDiag.ClusterInfo = &ClusterInfo{ + Nodes: len(clusterStatus), } // Get node details if nodes, err := client.GetNodes(ctx); err == nil && len(nodes) > 0 { - nodeDiag.Details = map[string]interface{}{ - "node_count": len(nodes), + nodeDiag.Details = &NodeDetails{ + NodeCount: len(nodes), } } } @@ -177,8 +192,8 @@ func (r *Router) handleDiagnostics(w http.ResponseWriter, req *http.Request) { pbsDiag.Error = "Connection established but version check failed: " + err.Error() } else { pbsDiag.Connected = true - pbsDiag.Details = map[string]interface{}{ - "version": version.Version, + pbsDiag.Details = &PBSDetails{ + Version: version.Version, } } } diff --git a/internal/api/router.go b/internal/api/router.go index aa9d7c6d7..703512b86 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -832,13 +833,13 @@ func (r *Router) handleHealth(w http.ResponseWriter, req *http.Request) { return } - health := map[string]interface{}{ - "status": "healthy", - "timestamp": time.Now().Unix(), - "uptime": time.Since(r.monitor.GetStartTime()).Seconds(), + response := HealthResponse{ + Status: "healthy", + Timestamp: time.Now().Unix(), + Uptime: time.Since(r.monitor.GetStartTime()).Seconds(), } - utils.WriteJSONResponse(w, health) + utils.WriteJSONResponse(w, response) } // handleChangePassword handles password change requests @@ -1119,21 +1120,25 @@ func (r *Router) handleVersion(w http.ResponseWriter, req *http.Request) { if err != nil { // Fallback to VERSION file versionBytes, _ := os.ReadFile("VERSION") - version := map[string]interface{}{ - "version": strings.TrimSpace(string(versionBytes)), - "build": "development", - "runtime": "go", + response := VersionResponse{ + Version: strings.TrimSpace(string(versionBytes)), + BuildTime: "development", + GoVersion: runtime.Version(), } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(version) + json.NewEncoder(w).Encode(response) return } - // Add update channel from config - versionInfo.Channel = r.config.UpdateChannel + // Convert to typed response + response := VersionResponse{ + Version: versionInfo.Version, + BuildTime: versionInfo.Build, + GoVersion: runtime.Version(), + } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(versionInfo) + json.NewEncoder(w).Encode(response) } // handleStorage handles storage detail requests @@ -1225,8 +1230,8 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { state := r.monitor.GetState() // Create chart data structure that matches frontend expectations - chartData := make(map[string]map[string][]map[string]interface{}) - nodeData := make(map[string]map[string][]map[string]interface{}) + chartData := make(map[string]VMChartData) + nodeData := make(map[string]NodeChartData) currentTime := time.Now().Unix() * 1000 // JavaScript timestamp format oldestTimestamp := currentTime @@ -1234,7 +1239,7 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Process VMs - get historical data for _, vm := range state.VMs { if chartData[vm.ID] == nil { - chartData[vm.ID] = make(map[string][]map[string]interface{}) + chartData[vm.ID] = make(VMChartData) } // Get historical metrics @@ -1242,41 +1247,41 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Convert metric points to API format for metricType, points := range metrics { - chartData[vm.ID][metricType] = make([]map[string]interface{}, len(points)) + chartData[vm.ID][metricType] = make([]MetricPoint, len(points)) for i, point := range points { ts := point.Timestamp.Unix() * 1000 if ts < oldestTimestamp { oldestTimestamp = ts } - chartData[vm.ID][metricType][i] = map[string]interface{}{ - "timestamp": ts, - "value": point.Value, + chartData[vm.ID][metricType][i] = MetricPoint{ + Timestamp: ts, + Value: point.Value, } } } // If no historical data, add current value if len(chartData[vm.ID]["cpu"]) == 0 { - chartData[vm.ID]["cpu"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.CPU * 100}, + chartData[vm.ID]["cpu"] = []MetricPoint{ + {Timestamp: currentTime, Value: vm.CPU * 100}, } - chartData[vm.ID]["memory"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.Memory.Usage}, + chartData[vm.ID]["memory"] = []MetricPoint{ + {Timestamp: currentTime, Value: vm.Memory.Usage}, } - chartData[vm.ID]["disk"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.Disk.Usage}, + chartData[vm.ID]["disk"] = []MetricPoint{ + {Timestamp: currentTime, Value: vm.Disk.Usage}, } - chartData[vm.ID]["diskread"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.DiskRead}, + chartData[vm.ID]["diskread"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(vm.DiskRead)}, } - chartData[vm.ID]["diskwrite"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.DiskWrite}, + chartData[vm.ID]["diskwrite"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(vm.DiskWrite)}, } - chartData[vm.ID]["netin"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.NetworkIn}, + chartData[vm.ID]["netin"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(vm.NetworkIn)}, } - chartData[vm.ID]["netout"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": vm.NetworkOut}, + chartData[vm.ID]["netout"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(vm.NetworkOut)}, } } } @@ -1284,7 +1289,7 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Process Containers - get historical data for _, ct := range state.Containers { if chartData[ct.ID] == nil { - chartData[ct.ID] = make(map[string][]map[string]interface{}) + chartData[ct.ID] = make(VMChartData) } // Get historical metrics @@ -1292,50 +1297,50 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Convert metric points to API format for metricType, points := range metrics { - chartData[ct.ID][metricType] = make([]map[string]interface{}, len(points)) + chartData[ct.ID][metricType] = make([]MetricPoint, len(points)) for i, point := range points { ts := point.Timestamp.Unix() * 1000 if ts < oldestTimestamp { oldestTimestamp = ts } - chartData[ct.ID][metricType][i] = map[string]interface{}{ - "timestamp": ts, - "value": point.Value, + chartData[ct.ID][metricType][i] = MetricPoint{ + Timestamp: ts, + Value: point.Value, } } } // If no historical data, add current value if len(chartData[ct.ID]["cpu"]) == 0 { - chartData[ct.ID]["cpu"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.CPU * 100}, + chartData[ct.ID]["cpu"] = []MetricPoint{ + {Timestamp: currentTime, Value: ct.CPU * 100}, } - chartData[ct.ID]["memory"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.Memory.Usage}, + chartData[ct.ID]["memory"] = []MetricPoint{ + {Timestamp: currentTime, Value: ct.Memory.Usage}, } - chartData[ct.ID]["disk"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.Disk.Usage}, + chartData[ct.ID]["disk"] = []MetricPoint{ + {Timestamp: currentTime, Value: ct.Disk.Usage}, } - chartData[ct.ID]["diskread"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.DiskRead}, + chartData[ct.ID]["diskread"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(ct.DiskRead)}, } - chartData[ct.ID]["diskwrite"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.DiskWrite}, + chartData[ct.ID]["diskwrite"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(ct.DiskWrite)}, } - chartData[ct.ID]["netin"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.NetworkIn}, + chartData[ct.ID]["netin"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(ct.NetworkIn)}, } - chartData[ct.ID]["netout"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": ct.NetworkOut}, + chartData[ct.ID]["netout"] = []MetricPoint{ + {Timestamp: currentTime, Value: float64(ct.NetworkOut)}, } } } // Process Storage - get historical data - storageData := make(map[string]map[string][]map[string]interface{}) + storageData := make(map[string]StorageChartData) for _, storage := range state.Storage { if storageData[storage.ID] == nil { - storageData[storage.ID] = make(map[string][]map[string]interface{}) + storageData[storage.ID] = make(StorageChartData) } // Get historical metrics @@ -1344,15 +1349,15 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Convert usage metrics to chart format if usagePoints, ok := metrics["usage"]; ok && len(usagePoints) > 0 { // Convert MetricPoint slice to chart format - storageData[storage.ID]["disk"] = make([]map[string]interface{}, len(usagePoints)) + storageData[storage.ID]["disk"] = make([]MetricPoint, len(usagePoints)) for i, point := range usagePoints { ts := point.Timestamp.Unix() * 1000 if ts < oldestTimestamp { oldestTimestamp = ts } - storageData[storage.ID]["disk"][i] = map[string]interface{}{ - "timestamp": ts, - "value": point.Value, + storageData[storage.ID]["disk"][i] = MetricPoint{ + Timestamp: ts, + Value: point.Value, } } } else { @@ -1361,8 +1366,8 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { if storage.Total > 0 { usagePercent = (float64(storage.Used) / float64(storage.Total)) * 100 } - storageData[storage.ID]["disk"] = []map[string]interface{}{ - {"timestamp": currentTime, "value": usagePercent}, + storageData[storage.ID]["disk"] = []MetricPoint{ + {Timestamp: currentTime, Value: usagePercent}, } } } @@ -1370,21 +1375,21 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { // Process Nodes - get historical data for _, node := range state.Nodes { if nodeData[node.ID] == nil { - nodeData[node.ID] = make(map[string][]map[string]interface{}) + nodeData[node.ID] = make(NodeChartData) } // Get historical metrics for each type for _, metricType := range []string{"cpu", "memory", "disk"} { points := r.monitor.GetNodeMetrics(node.ID, metricType, duration) - nodeData[node.ID][metricType] = make([]map[string]interface{}, len(points)) + nodeData[node.ID][metricType] = make([]MetricPoint, len(points)) for i, point := range points { ts := point.Timestamp.Unix() * 1000 if ts < oldestTimestamp { oldestTimestamp = ts } - nodeData[node.ID][metricType][i] = map[string]interface{}{ - "timestamp": ts, - "value": point.Value, + nodeData[node.ID][metricType][i] = MetricPoint{ + Timestamp: ts, + Value: point.Value, } } @@ -1399,20 +1404,20 @@ func (r *Router) handleCharts(w http.ResponseWriter, req *http.Request) { case "disk": value = node.Disk.Usage } - nodeData[node.ID][metricType] = []map[string]interface{}{ - {"timestamp": currentTime, "value": value}, + nodeData[node.ID][metricType] = []MetricPoint{ + {Timestamp: currentTime, Value: value}, } } } } - response := map[string]interface{}{ - "data": chartData, - "nodeData": nodeData, - "storageData": storageData, - "timestamp": currentTime, - "stats": map[string]interface{}{ - "oldestDataTimestamp": oldestTimestamp, + response := ChartResponse{ + ChartData: chartData, + NodeData: nodeData, + StorageData: storageData, + Timestamp: currentTime, + Stats: ChartStats{ + OldestDataTimestamp: oldestTimestamp, }, } @@ -1449,16 +1454,16 @@ func (r *Router) handleStorageCharts(w http.ResponseWriter, req *http.Request) { state := r.monitor.GetState() // Build storage chart data - storageData := make(map[string]interface{}) + storageData := make(StorageChartsResponse) for _, storage := range state.Storage { metrics := r.monitor.GetStorageMetrics(storage.ID, duration) - storageData[storage.ID] = map[string]interface{}{ - "usage": metrics["usage"], - "used": metrics["used"], - "total": metrics["total"], - "avail": metrics["avail"], + storageData[storage.ID] = StorageMetrics{ + Usage: metrics["usage"], + Used: metrics["used"], + Total: metrics["total"], + Avail: metrics["avail"], } } diff --git a/internal/api/types.go b/internal/api/types.go new file mode 100644 index 000000000..2cc76157c --- /dev/null +++ b/internal/api/types.go @@ -0,0 +1,388 @@ +package api + +import ( + "time" + "github.com/rcourtman/pulse-go-rewrite/internal/models" + "github.com/rcourtman/pulse-go-rewrite/internal/types" +) + +// Common response types for API endpoints + +// HealthResponse represents the health check response +type HealthResponse struct { + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + Uptime float64 `json:"uptime"` +} + +// VersionResponse represents version information +type VersionResponse struct { + Version string `json:"version"` + BuildTime string `json:"buildTime,omitempty"` + GoVersion string `json:"goVersion,omitempty"` + UpdateAvailable bool `json:"updateAvailable"` + LatestVersion string `json:"latestVersion,omitempty"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` + Code int `json:"code,omitempty"` +} + +// SuccessResponse represents a generic success response +type SuccessResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +// StateResponse represents the full state response +type StateResponse struct { + Nodes []models.Node `json:"nodes"` + VMs []models.VM `json:"vms"` + Containers []models.Container `json:"containers"` + Storage []models.Storage `json:"storage"` + PBSInstances []models.PBSInstance `json:"pbs"` + PBSBackups []models.PBSBackup `json:"pbsBackups"` + Metrics []models.Metric `json:"metrics"` + PVEBackups models.PVEBackups `json:"pveBackups"` + Performance models.Performance `json:"performance"` + ConnectionHealth map[string]bool `json:"connectionHealth"` + Stats models.Stats `json:"stats"` + ActiveAlerts []models.Alert `json:"activeAlerts"` + RecentlyResolved []models.ResolvedAlert `json:"recentlyResolved"` + LastUpdate time.Time `json:"lastUpdate"` +} + +// ConfigResponse represents configuration response +type ConfigResponse struct { + Nodes []NodeConfig `json:"nodes"` + Settings SettingsConfig `json:"settings"` +} + +// NodeConfig represents a node configuration +type NodeConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Address string `json:"address"` + Port int `json:"port,omitempty"` + Username string `json:"username,omitempty"` + HasPassword bool `json:"hasPassword"` + HasToken bool `json:"hasToken"` + SkipTLS bool `json:"skipTLS,omitempty"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// SettingsConfig represents application settings +type SettingsConfig struct { + CheckInterval int `json:"checkInterval"` + RetentionDays int `json:"retentionDays"` + Theme string `json:"theme,omitempty"` + TimeZone string `json:"timezone,omitempty"` + NotificationsOn bool `json:"notificationsOn"` +} + +// NodeRequest represents a request to create/update a node +type NodeRequest struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Type string `json:"type" validate:"required,oneof=proxmox pve pbs"` + Address string `json:"address" validate:"required,ip|hostname"` + Port int `json:"port,omitempty" validate:"omitempty,min=1,max=65535"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + SkipTLS bool `json:"skipTLS,omitempty"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// SettingsRequest represents a request to update settings +type SettingsRequest struct { + CheckInterval *int `json:"checkInterval,omitempty" validate:"omitempty,min=10,max=3600"` + RetentionDays *int `json:"retentionDays,omitempty" validate:"omitempty,min=1,max=365"` + Theme *string `json:"theme,omitempty" validate:"omitempty,oneof=light dark auto"` + TimeZone *string `json:"timezone,omitempty"` + NotificationsOn *bool `json:"notificationsOn,omitempty"` +} + +// BackupResponse represents backup information +type BackupResponse struct { + Backups []BackupInfo `json:"backups"` + Total int `json:"total"` +} + +// BackupInfo represents a single backup +type BackupInfo struct { + ID string `json:"id"` + VMID string `json:"vmid"` + Name string `json:"name"` + Type string `json:"type"` + Size int64 `json:"size"` + Time time.Time `json:"time"` + Node string `json:"node"` + Storage string `json:"storage,omitempty"` + Status string `json:"status"` + Notes string `json:"notes,omitempty"` +} + +// MetricsResponse represents metrics data +type MetricsResponse struct { + Metrics map[string]MetricData `json:"metrics"` + Period string `json:"period"` +} + +// MetricData represents metric data points +type MetricData struct { + Values []float64 `json:"values"` + Timestamps []time.Time `json:"timestamps"` + Unit string `json:"unit,omitempty"` +} + +// StorageResponse represents storage information +type StorageResponse struct { + Storage []StorageInfo `json:"storage"` + Total StorageTotals `json:"totals"` +} + +// StorageInfo represents storage details +type StorageInfo struct { + ID string `json:"id"` + Node string `json:"node"` + Storage string `json:"storage"` + Type string `json:"type"` + Status string `json:"status"` + Total int64 `json:"total"` + Used int64 `json:"used"` + Available int64 `json:"available"` + Percentage float64 `json:"percentage"` +} + +// StorageTotals represents aggregate storage metrics +type StorageTotals struct { + Total int64 `json:"total"` + Used int64 `json:"used"` + Available int64 `json:"available"` + Percentage float64 `json:"percentage"` +} + +// ChartResponse represents chart data +type ChartResponse struct { + ChartData map[string]VMChartData `json:"data"` + NodeData map[string]NodeChartData `json:"nodeData"` + StorageData map[string]StorageChartData `json:"storageData"` + Timestamp int64 `json:"timestamp"` + Stats ChartStats `json:"stats"` +} + +// ChartStats represents chart statistics +type ChartStats struct { + OldestDataTimestamp int64 `json:"oldestDataTimestamp"` +} + +// VMChartData represents chart data for a VM +type VMChartData map[string][]MetricPoint + +// NodeChartData represents chart data for a node +type NodeChartData map[string][]MetricPoint + +// StorageChartData represents chart data for storage +type StorageChartData map[string][]MetricPoint + +// StorageChartsResponse represents storage charts API response +type StorageChartsResponse map[string]StorageMetrics + +// StorageMetrics represents storage metrics data +type StorageMetrics struct { + Usage []types.MetricPoint `json:"usage"` + Used []types.MetricPoint `json:"used"` + Total []types.MetricPoint `json:"total"` + Avail []types.MetricPoint `json:"avail"` +} + +// MetricPoint represents a single metric data point +type MetricPoint struct { + Timestamp int64 `json:"timestamp"` + Value float64 `json:"value"` +} + +// ChartData represents data for a single chart +type ChartData struct { + Labels []string `json:"labels"` + Datasets []Dataset `json:"datasets"` +} + +// Dataset represents a chart dataset +type Dataset struct { + Label string `json:"label"` + Data []float64 `json:"data"` + BackgroundColor string `json:"backgroundColor,omitempty"` + BorderColor string `json:"borderColor,omitempty"` +} + +// DiagnosticsResponse represents system diagnostics +type DiagnosticsResponse struct { + System SystemInfo `json:"system"` + Connections []ConnectionInfo `json:"connections"` + Errors []ErrorInfo `json:"errors"` + Performance PerformanceInfo `json:"performance"` +} + +// SystemInfo represents system information +type SystemInfo struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + Arch string `json:"arch"` + CPUCount int `json:"cpuCount"` + Memory int64 `json:"memory"` + GoVersion string `json:"goVersion"` + Uptime float64 `json:"uptime"` + StartTime time.Time `json:"startTime"` +} + +// ConnectionInfo represents connection status +type ConnectionInfo struct { + Node string `json:"node"` + Type string `json:"type"` + Address string `json:"address"` + Status string `json:"status"` + Latency time.Duration `json:"latency,omitempty"` + LastSeen time.Time `json:"lastSeen,omitempty"` + Error string `json:"error,omitempty"` +} + +// ErrorInfo represents error information +type ErrorInfo struct { + Time time.Time `json:"time"` + Level string `json:"level"` + Message string `json:"message"` + Source string `json:"source,omitempty"` +} + +// PerformanceInfo represents performance metrics +type PerformanceInfo struct { + CPUUsage float64 `json:"cpuUsage"` + MemoryUsage int64 `json:"memoryUsage"` + Goroutines int `json:"goroutines"` + RequestRate float64 `json:"requestRate"` + ErrorRate float64 `json:"errorRate"` +} + +// SecurityStatusResponse represents security configuration status +type SecurityStatusResponse struct { + Configured bool `json:"configured"` + Method string `json:"method"` + RequiresSetup bool `json:"requiresSetup"` + DeploymentType string `json:"deploymentType,omitempty"` +} + +// ExportRequest represents a configuration export request +type ExportRequest struct { + Passphrase string `json:"passphrase" validate:"required,min=8"` + IncludeCredentials bool `json:"includeCredentials,omitempty"` +} + +// ImportRequest represents a configuration import request +type ImportRequest struct { + Data string `json:"data" validate:"required"` + Passphrase string `json:"passphrase" validate:"required"` + Overwrite bool `json:"overwrite,omitempty"` +} + +// NotificationTestRequest represents a notification test request +type NotificationTestRequest struct { + Type string `json:"type" validate:"required,oneof=webhook discord slack email"` + Config map[string]interface{} `json:"config" validate:"required"` + Message string `json:"message,omitempty"` +} + +// UpdateCheckResponse represents update availability +type UpdateCheckResponse struct { + CurrentVersion string `json:"currentVersion"` + LatestVersion string `json:"latestVersion"` + UpdateAvailable bool `json:"updateAvailable"` + ReleaseNotes string `json:"releaseNotes,omitempty"` + ReleaseDate time.Time `json:"releaseDate,omitempty"` + DownloadURL string `json:"downloadUrl,omitempty"` +} + +// WebSocketMessage represents a WebSocket message +type WebSocketMessage struct { + Type string `json:"type"` + Data interface{} `json:"data"` + Timestamp time.Time `json:"timestamp"` +} + +// LoginRequest represents a login request (if implemented) +type LoginRequest struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` +} + +// LoginResponse represents a login response +type LoginResponse struct { + Success bool `json:"success"` + Token string `json:"token,omitempty"` + Message string `json:"message,omitempty"` +} + +// TestConnectionResponse represents a connection test result +type TestConnectionResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Latency int64 `json:"latency,omitempty"` + Details string `json:"details,omitempty"` +} + +// NodeConnectionResponse represents node connection result +type NodeConnectionResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Nodes int `json:"nodes,omitempty"` +} + +// DiscoveryResponse represents discovery results +type DiscoveryResponse struct { + Servers []DiscoveredServer `json:"servers"` + Errors []string `json:"errors"` + Cached bool `json:"cached"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` +} + +// DiscoveredServer represents a discovered server +type DiscoveredServer struct { + IP string `json:"ip"` + Port int `json:"port"` + Type string `json:"type"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Status string `json:"status,omitempty"` +} + +// AutoRegisterResponse represents auto-registration response +type AutoRegisterResponse struct { + Status string `json:"status"` + Message string `json:"message"` + TokenID string `json:"tokenId,omitempty"` + TokenName string `json:"tokenName,omitempty"` +} + +// ConfigImportResponse represents import response +type ConfigImportResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +// ConfigExportResponse represents export response +type ConfigExportResponse struct { + Status string `json:"status"` + Data string `json:"data"` +} + +// InstallScriptResponse represents install script response +type InstallScriptResponse struct { + URL string `json:"url"` + Command string `json:"command"` +} \ No newline at end of file