diff --git a/frontend-modern/src/components/Settings/NodeModal.tsx b/frontend-modern/src/components/Settings/NodeModal.tsx index 0d6080877..eb50bf279 100644 --- a/frontend-modern/src/components/Settings/NodeModal.tsx +++ b/frontend-modern/src/components/Settings/NodeModal.tsx @@ -1,12 +1,13 @@ import { Component, Show, createSignal, createEffect } from 'solid-js'; import { Portal } from 'solid-js/web'; +import type { NodeConfig } from '@/types/nodes'; interface NodeModalProps { isOpen: boolean; onClose: () => void; nodeType: 'pve' | 'pbs'; - editingNode?: any; - onSave: (nodeData: any) => void; + editingNode?: NodeConfig; + onSave: (nodeData: Partial) => void; } export const NodeModal: Component = (props) => { @@ -36,25 +37,26 @@ export const NodeModal: Component = (props) => { // Update form when editing node changes createEffect(() => { if (props.editingNode) { + const node = props.editingNode; setFormData({ - name: props.editingNode.name || '', - host: props.editingNode.host || '', - authType: props.editingNode.user ? 'password' : 'token', - user: props.editingNode.user || '', + name: node.name || '', + host: node.host || '', + authType: node.user ? 'password' : 'token', + user: node.user || '', password: '', // Don't show existing password - tokenName: props.editingNode.tokenName || '', + tokenName: node.tokenName || '', tokenValue: '', // Don't show existing token - fingerprint: props.editingNode.fingerprint || '', - verifySSL: props.editingNode.verifySSL ?? true, - monitorVMs: props.editingNode.monitorVMs ?? true, - monitorContainers: props.editingNode.monitorContainers ?? true, - monitorStorage: props.editingNode.monitorStorage ?? true, - monitorBackups: props.editingNode.monitorBackups ?? true, - monitorDatastores: props.editingNode.monitorDatastores ?? true, - monitorSyncJobs: props.editingNode.monitorSyncJobs ?? true, - monitorVerifyJobs: props.editingNode.monitorVerifyJobs ?? true, - monitorPruneJobs: props.editingNode.monitorPruneJobs ?? true, - monitorGarbageJobs: props.editingNode.monitorGarbageJobs ?? false + fingerprint: ('fingerprint' in node ? node.fingerprint : '') || '', + verifySSL: node.verifySSL ?? true, + monitorVMs: (node.type === 'pve' && 'monitorVMs' in node ? node.monitorVMs : true) ?? true, + monitorContainers: (node.type === 'pve' && 'monitorContainers' in node ? node.monitorContainers : true) ?? true, + monitorStorage: (node.type === 'pve' && 'monitorStorage' in node ? node.monitorStorage : true) ?? true, + monitorBackups: (node.type === 'pve' && 'monitorBackups' in node ? node.monitorBackups : true) ?? true, + monitorDatastores: (node.type === 'pbs' && 'monitorDatastores' in node ? node.monitorDatastores : true) ?? true, + monitorSyncJobs: (node.type === 'pbs' && 'monitorSyncJobs' in node ? node.monitorSyncJobs : true) ?? true, + monitorVerifyJobs: (node.type === 'pbs' && 'monitorVerifyJobs' in node ? node.monitorVerifyJobs : true) ?? true, + monitorPruneJobs: (node.type === 'pbs' && 'monitorPruneJobs' in node ? node.monitorPruneJobs : true) ?? true, + monitorGarbageJobs: (node.type === 'pbs' && 'monitorGarbageJobs' in node ? node.monitorGarbageJobs : false) ?? false }); } else { // Reset form for new node @@ -86,7 +88,7 @@ export const NodeModal: Component = (props) => { const data = formData(); // Prepare data based on auth type - const nodeData: Partial = { + const nodeData: Partial = { type: props.nodeType, name: data.name, host: data.host, @@ -108,22 +110,22 @@ export const NodeModal: Component = (props) => { // Add monitor settings based on type if (props.nodeType === 'pve') { - nodeData.monitorVMs = data.monitorVMs; - nodeData.monitorContainers = data.monitorContainers; - nodeData.monitorStorage = data.monitorStorage; - nodeData.monitorBackups = data.monitorBackups; + (nodeData as any).monitorVMs = data.monitorVMs; + (nodeData as any).monitorContainers = data.monitorContainers; + (nodeData as any).monitorStorage = data.monitorStorage; + (nodeData as any).monitorBackups = data.monitorBackups; } else { - nodeData.monitorDatastores = data.monitorDatastores; - nodeData.monitorSyncJobs = data.monitorSyncJobs; - nodeData.monitorVerifyJobs = data.monitorVerifyJobs; - nodeData.monitorPruneJobs = data.monitorPruneJobs; - nodeData.monitorGarbageJobs = data.monitorGarbageJobs; + (nodeData as any).monitorDatastores = data.monitorDatastores; + (nodeData as any).monitorSyncJobs = data.monitorSyncJobs; + (nodeData as any).monitorVerifyJobs = data.monitorVerifyJobs; + (nodeData as any).monitorPruneJobs = data.monitorPruneJobs; + (nodeData as any).monitorGarbageJobs = data.monitorGarbageJobs; } props.onSave(nodeData); }; - const updateField = (field: string, value: any) => { + const updateField = (field: string, value: string | boolean) => { setFormData(prev => ({ ...prev, [field]: value })); }; diff --git a/frontend-modern/src/components/shared/DynamicChart.tsx b/frontend-modern/src/components/shared/DynamicChart.tsx index cb9d741b7..2d39d527a 100644 --- a/frontend-modern/src/components/shared/DynamicChart.tsx +++ b/frontend-modern/src/components/shared/DynamicChart.tsx @@ -2,9 +2,10 @@ import { Component, onMount, onCleanup, createMemo, createEffect, Show } from 's import Sparkline from '@/components/Dashboard/Sparkline'; import { observeChart, subscribeToChartDimension } from '@/stores/chartDimensions'; import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; +import type { ChartPoint } from '@/types/charts'; interface DynamicChartProps { - data: any; + data: ChartPoint[] | undefined; metric: string; guestId: string; chartType?: 'mini' | 'sparkline' | 'storage'; diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 21631d40f..291ba7fd8 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -7,6 +7,9 @@ import { useWebSocket } from '@/App'; import { showSuccess, showError } from '@/utils/toast'; import { AlertsAPI } from '@/api/alerts'; import { NotificationsAPI } from '@/api/notifications'; +import type { EmailConfig, Webhook } from '@/api/notifications'; +import type { HysteresisThreshold, AlertThresholds } from '@/types/alerts'; +import type { Node, VM, Container, Storage, Alert } from '@/types/api'; type AlertTab = 'overview' | 'thresholds' | 'destinations' | 'schedule' | 'history' | 'custom-rules'; @@ -37,8 +40,28 @@ export function Alerts() { const [hasUnsavedChanges, setHasUnsavedChanges] = createSignal(false); // Store references to child component data - let destinationsRef: any = {}; - let scheduleRef: any = {}; + interface DestinationsRef { + emailConfig?: () => EmailConfig; + } + + interface ScheduleConfig { + enabled?: boolean; + quietHours?: { + enabled: boolean; + start: string; + end: string; + days: Record; + }; + cooldown?: number; + groupingWindow?: number; + } + + interface ScheduleRef { + setScheduleConfig?: (config: ScheduleConfig) => void; + } + + let destinationsRef: DestinationsRef = {}; + let scheduleRef: ScheduleRef = {}; const [overrides, setOverrides] = createSignal([]); @@ -76,18 +99,18 @@ export function Alerts() { Object.entries(config.overrides).forEach(([key, thresholds]) => { // Check if it's a node override by looking for matching node - const node = (state.nodes || []).find((n: any) => n.id === key); + const node = (state.nodes || []).find((n) => n.id === key); if (node) { overridesList.push({ id: key, name: node.name, type: 'node', resourceType: 'Node', - thresholds: extractTriggerValues(thresholds as any) + thresholds: extractTriggerValues(thresholds) }); } else { // Find the guest by matching the full ID - const guest = [...(state.vms || []), ...(state.containers || [])].find((g: any) => g.id === key); + const guest = [...(state.vms || []), ...(state.containers || [])].find((g) => g.id === key); if (guest) { overridesList.push({ id: key, @@ -97,7 +120,7 @@ export function Alerts() { vmid: guest.vmid, node: guest.node, instance: guest.instance, - thresholds: extractTriggerValues(thresholds as any) + thresholds: extractTriggerValues(thresholds) }); } } @@ -132,19 +155,19 @@ export function Alerts() { }}); // Helper function to extract trigger value from threshold - const getTriggerValue = (threshold: any): number => { + const getTriggerValue = (threshold: number | HysteresisThreshold | undefined): number => { if (typeof threshold === 'number') { return threshold; // Legacy format } - if (threshold && typeof threshold === 'object' && threshold.trigger) { + if (threshold && typeof threshold === 'object' && 'trigger' in threshold) { return threshold.trigger; // New hysteresis format } return 0; // Default fallback }; // Helper to extract trigger values for all thresholds - const extractTriggerValues = (thresholds: any) => { - const result: any = {}; + const extractTriggerValues = (thresholds: AlertThresholds): Record => { + const result: Record = {}; Object.entries(thresholds).forEach(([key, value]) => { result[key] = getTriggerValue(value); }); @@ -261,7 +284,7 @@ export function Alerts() { hysteresisMargin: 5.0, overrides: overrides().reduce((acc, o) => { // Convert thresholds to hysteresis format - const hysteresisThresholds: any = {}; + const hysteresisThresholds: AlertThresholds = {}; Object.entries(o.thresholds).forEach(([metric, value]) => { hysteresisThresholds[metric] = createHysteresisThreshold(value as number); }); @@ -416,7 +439,7 @@ export function Alerts() { } // Overview Tab - Shows current alert status -function OverviewTab(props: { overrides: any[]; activeAlerts: Record }) { +function OverviewTab(props: { overrides: Override[]; activeAlerts: Record }) { // Get alert stats from actual active alerts const alertStats = createMemo(() => { const alerts = Object.values(props.activeAlerts); @@ -507,7 +530,7 @@ function OverviewTab(props: { overrides: any[]; activeAlerts: Record
- {(alert: any) => ( + {(alert) => (
= {}; + const thresholdValues = thresholds(); Object.entries(enabledThresholds()).forEach(([key, enabled]) => { - if (enabled && (thresholds() as any)[key]) { - activeThresholds[key] = (thresholds() as any)[key]; + if (enabled && key in thresholdValues) { + activeThresholds[key] = thresholdValues[key as keyof typeof thresholdValues]; } }); @@ -711,7 +735,7 @@ function AddOverrideForm(props: { {(resource) => ( )} @@ -732,10 +756,10 @@ function AddOverrideForm(props: { - {(thresholds() as any)[metric]}% + {thresholds()[metric as keyof typeof thresholds]}%
{ setThresholds({ ...thresholds(), @@ -837,10 +861,10 @@ function OverrideItem(props: { - {(editValues() as any)[key]}% + {editValues()[key as keyof typeof editValues]}%
setEditValues({ ...editValues(), [key]: value @@ -896,7 +920,21 @@ function OverrideItem(props: { } // Thresholds Tab - Improved design -function ThresholdsTab(props: any) { +interface ThresholdsTabProps { + allGuests: () => Array; + allNodes: () => Node[]; + guestDefaults: () => Record; + nodeDefaults: () => Record; + storageDefault: () => number; + overrides: () => Override[]; + setGuestDefaults: (value: Record | ((prev: Record) => Record)) => void; + setNodeDefaults: (value: Record | ((prev: Record) => Record)) => void; + setStorageDefault: (value: number) => void; + setOverrides: (value: Override[]) => void; + activeAlerts: () => Record; +} + +function ThresholdsTab(props: ThresholdsTabProps) { return (
{/* Step 1: Global Default Thresholds */} @@ -954,7 +992,7 @@ function ThresholdsTab(props: any) { { - props.setGuestDefaults((prev: any) => ({...prev, cpu: v})); + props.setGuestDefaults((prev) => ({...prev, cpu: v})); props.onChangeAny(); }} type="cpu" @@ -969,7 +1007,7 @@ function ThresholdsTab(props: any) { { - props.setGuestDefaults((prev: any) => ({...prev, memory: v})); + props.setGuestDefaults((prev) => ({...prev, memory: v})); props.onChangeAny(); }} type="memory" @@ -984,7 +1022,7 @@ function ThresholdsTab(props: any) { { - props.setGuestDefaults((prev: any) => ({...prev, disk: v})); + props.setGuestDefaults((prev) => ({...prev, disk: v})); props.onChangeAny(); }} type="disk" @@ -1011,7 +1049,7 @@ function ThresholdsTab(props: any) { { - props.setNodeDefaults((prev: any) => ({...prev, cpu: v})); + props.setNodeDefaults((prev) => ({...prev, cpu: v})); props.onChangeAny(); }} type="cpu" @@ -1026,7 +1064,7 @@ function ThresholdsTab(props: any) { { - props.setNodeDefaults((prev: any) => ({...prev, memory: v})); + props.setNodeDefaults((prev) => ({...prev, memory: v})); props.onChangeAny(); }} type="memory" @@ -1041,7 +1079,7 @@ function ThresholdsTab(props: any) { { - props.setNodeDefaults((prev: any) => ({...prev, disk: v})); + props.setNodeDefaults((prev) => ({...prev, disk: v})); props.onChangeAny(); }} type="disk" @@ -1108,7 +1146,7 @@ function ThresholdsTab(props: any) { props.onChangeAny(); }} onRemove={() => { - props.setOverrides(props.overrides.filter((o: any) => o.id !== override.id)); + props.setOverrides(props.overrides.filter((o) => o.id !== override.id)); props.onChangeAny(); }} /> @@ -1149,7 +1187,13 @@ function ThresholdsTab(props: any) { } // Destinations Tab - Notification settings -function DestinationsTab(props: any) { +interface DestinationsTabProps { + ref: DestinationsRef; + hasUnsavedChanges: () => boolean; + setHasUnsavedChanges: (value: boolean) => void; +} + +function DestinationsTab(props: DestinationsTabProps) { // Expose emailConfig to parent onMount(() => { if (props.ref) { @@ -1173,7 +1217,7 @@ function DestinationsTab(props: any) { rateLimit: 60 }); - const [webhooks, setWebhooks] = createSignal([]); + const [webhooks, setWebhooks] = createSignal([]); const [testingEmail, setTestingEmail] = createSignal(false); const [testingWebhook, setTestingWebhook] = createSignal(null); @@ -1313,7 +1357,13 @@ function DestinationsTab(props: any) { // History Tab - Alert history // Schedule Tab - Quiet hours, cooldown, and grouping -function ScheduleTab(props: any) { +interface ScheduleTabProps { + ref: ScheduleRef; + hasUnsavedChanges: () => boolean; + setHasUnsavedChanges: (value: boolean) => void; +} + +function ScheduleTab(props: ScheduleTabProps) { const [quietHours, setQuietHours] = createSignal({ enabled: false, start: '22:00', @@ -1387,7 +1437,7 @@ function ScheduleTab(props: any) { grouping: grouping() }); - props.ref.setScheduleConfig = (config: any) => { + props.ref.setScheduleConfig = (config: ScheduleConfig) => { if (config.quietHours) { setQuietHours(prev => ({ ...prev, @@ -1512,7 +1562,7 @@ function ScheduleTab(props: any) { {(day) => (

- + Weekdays only - + Weekends only

@@ -1897,7 +1947,7 @@ function HistoryTab() { const [timeFilter, setTimeFilter] = createSignal(localStorage.getItem('alertHistoryTimeFilter') || '7d'); const [severityFilter, setSeverityFilter] = createSignal(localStorage.getItem('alertHistorySeverityFilter') || 'all'); const [searchTerm, setSearchTerm] = createSignal(''); - const [alertHistory, setAlertHistory] = createSignal([]); + const [alertHistory, setAlertHistory] = createSignal([]); const [loading, setLoading] = createSignal(true); const [selectedBarIndex, setSelectedBarIndex] = createSignal(null); @@ -1978,18 +2028,18 @@ function HistoryTab() { // Get resource type (VM, CT, Node, Storage) const getResourceType = (resourceName: string) => { // Check VMs and containers - const vm = state.vms?.find((v: any) => v.name === resourceName); + const vm = state.vms?.find((v) => v.name === resourceName); if (vm) return 'VM'; - const container = state.containers?.find((c: any) => c.name === resourceName); + const container = state.containers?.find((c) => c.name === resourceName); if (container) return 'CT'; // Check nodes - const node = state.nodes?.find((n: any) => n.name === resourceName); + const node = state.nodes?.find((n) => n.name === resourceName); if (node) return 'Node'; // Check storage - const storage = state.storage?.find((s: any) => s.name === resourceName || s.id === resourceName); + const storage = state.storage?.find((s) => s.name === resourceName || s.id === resourceName); if (storage) return 'Storage'; return 'Unknown'; @@ -1998,10 +2048,10 @@ function HistoryTab() { // Prepare all alerts without filtering const allAlertsData = createMemo(() => { // Combine active and historical alerts - const allAlerts: any[] = []; + const allAlerts: Alert[] = []; // Add active alerts - Object.values(activeAlerts || {}).forEach((alert: any) => { + Object.values(activeAlerts || {}).forEach((alert) => { allAlerts.push({ ...alert, status: 'active', diff --git a/frontend-modern/src/stores/websocket.ts b/frontend-modern/src/stores/websocket.ts index 30e48a10d..8f9044189 100644 --- a/frontend-modern/src/stores/websocket.ts +++ b/frontend-modern/src/stores/websocket.ts @@ -1,6 +1,6 @@ import { createSignal, onCleanup } from 'solid-js'; import { createStore } from 'solid-js/store'; -import type { State, WSMessage, Alert, ResolvedAlert } from '@/types/api'; +import type { State, WSMessage, Alert, ResolvedAlert, PVEBackups, Performance, Stats } from '@/types/api'; import { logger } from '@/utils/logger'; import { POLLING_INTERVALS, WEBSOCKET } from '@/constants'; @@ -15,11 +15,30 @@ export function createWebSocketStore(url: string) { storage: [], pbs: [], metrics: [], - pveBackups: {} as any, + pveBackups: { + backupTasks: [], + storageBackups: [], + guestSnapshots: [] + } as PVEBackups, pbsBackups: [], - performance: {} as any, + performance: { + requestCount: 0, + errorCount: 0, + avgResponseTime: 0, + throughput: 0 + } as Performance, connectionHealth: {}, - stats: {} as any, + stats: { + totalNodes: 0, + totalVMs: 0, + totalContainers: 0, + totalCPU: 0, + totalMemory: 0, + totalDisk: 0, + totalBackups: 0, + oldestBackup: '', + newestBackup: '' + } as Stats, activeAlerts: [], recentlyResolved: [], lastUpdate: ''