diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index c7806c348..64badb006 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -1,4 +1,4 @@ -import { Show, createSignal, createContext, useContext, createEffect, onMount, onCleanup } from 'solid-js'; +import { Show, createSignal, createContext, useContext, createEffect, onMount, onCleanup, getOwner, runWithOwner } from 'solid-js'; import { getGlobalWebSocketStore } from './stores/websocket-global'; import { Dashboard } from './components/Dashboard/Dashboard'; import StorageComponent from './components/Storage/Storage'; @@ -46,6 +46,9 @@ export const useDarkMode = () => { }; function App() { + const owner = getOwner(); + const acquireWsStore = () => (owner ? runWithOwner(owner, () => getGlobalWebSocketStore()) : getGlobalWebSocketStore()); + // Simple auth state const [isLoading, setIsLoading] = createSignal(true); const [needsAuth, setNeedsAuth] = createSignal(false); @@ -189,7 +192,7 @@ function App() { setHasAuth(false); setNeedsAuth(false); // Initialize WebSocket immediately since no auth needed - setWsStore(getGlobalWebSocketStore()); + setWsStore(acquireWsStore()); // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists @@ -237,7 +240,7 @@ function App() { }); setNeedsAuth(false); // Initialize WebSocket for proxy auth users - setWsStore(getGlobalWebSocketStore()); + setWsStore(acquireWsStore()); // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists @@ -294,7 +297,7 @@ function App() { } else { setNeedsAuth(false); // Only initialize WebSocket after successful auth check - setWsStore(getGlobalWebSocketStore()); + setWsStore(acquireWsStore()); // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists @@ -324,7 +327,7 @@ function App() { console.error('Auth check error:', error); // On error, try to proceed without auth setNeedsAuth(false); - setWsStore(getGlobalWebSocketStore()); + setWsStore(acquireWsStore()); // Theme is already applied on initialization, no need to reapply } finally { diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index 800ff03fa..509f20e84 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -71,10 +71,7 @@ export function ResourceTable(props: ResourceTableProps) {
-
- +
diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 19121346b..08fbd6160 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -624,210 +624,257 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
- {/* Threshold table */} + {/* Threshold inputs in a responsive layout */}

Default thresholds for all resources. Individual resources can override these values below. Enter 0 or -1 to disable specific alerts.

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Resource TypeCPU
%
Memory
%
Disk
%
Storage
%
Disk Read
MB/s
Disk Write
MB/s
Net In
MB/s
Net Out
MB/s
VMs & Containers - { - props.setGuestDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setGuestDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setGuestDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - - { - props.setGuestDefaults((prev) => ({...prev, diskRead: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setGuestDefaults((prev) => ({...prev, diskWrite: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setGuestDefaults((prev) => ({...prev, networkIn: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setGuestDefaults((prev) => ({...prev, networkOut: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
Proxmox Nodes - { - props.setNodeDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setNodeDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - { - props.setNodeDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0})); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - -----
Storage--- - { - props.setStorageDefault(parseInt(e.currentTarget.value) || 0); - props.setHasUnsavedChanges(true); - }} - class="w-16 px-2 py-1 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - ----
+
+
+

+ VMs & Containers +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, diskRead: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, diskWrite: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, networkIn: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ ...prev, networkOut: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+
+
+

+ Proxmox Nodes +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+
+
+

+ Storage +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setStorageDefault(Number.isNaN(value) ? 0 : value); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+
- {/* Alert delay settings per resource type */}
diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index d1ac4522b..0947b193c 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -66,6 +66,75 @@ interface UIEmailConfig { rateLimit: number; } +interface QuietHoursConfig { + enabled: boolean; + start: string; + end: string; + timezone: string; + days: Record; +} + +interface CooldownConfig { + enabled: boolean; + minutes: number; + maxAlerts: number; +} + +interface GroupingConfig { + enabled: boolean; + window: number; + maxGroupSize?: number; + byNode?: boolean; + byGuest?: boolean; +} + +interface EscalationConfig { + enabled: boolean; + timeToEscalate?: number; + levels: Array<{ + level?: number; + destinations?: string[]; + after?: number; + notify?: string; + }>; +} + +const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + +const createDefaultQuietHours = (): QuietHoursConfig => ({ + enabled: false, + start: '22:00', + end: '08:00', + timezone: getLocalTimezone(), + days: { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + sunday: false + } +}); + +const createDefaultCooldown = (): CooldownConfig => ({ + enabled: true, + minutes: 30, + maxAlerts: 3 +}); + +const createDefaultGrouping = (): GroupingConfig => ({ + enabled: true, + window: 5, + byNode: true, + byGuest: false +}); + +const createDefaultEscalation = (): EscalationConfig => ({ + enabled: false, + levels: [] +}); + export function Alerts() { const { state, activeAlerts, updateAlert } = useWebSocket(); const [activeTab, setActiveTab] = createSignal('overview'); @@ -107,39 +176,13 @@ export function Alerts() { }); // Schedule configuration state moved to parent to persist across tab changes - const [scheduleQuietHours, setScheduleQuietHours] = createSignal({ - enabled: false, - start: '22:00', - end: '08:00', - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', - days: { - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: false, - sunday: false - } as Record - }); + const [scheduleQuietHours, setScheduleQuietHours] = createSignal(createDefaultQuietHours()); - const [scheduleCooldown, setScheduleCooldown] = createSignal({ - enabled: true, - minutes: 30, - maxAlerts: 3 - }); + const [scheduleCooldown, setScheduleCooldown] = createSignal(createDefaultCooldown()); - const [scheduleGrouping, setScheduleGrouping] = createSignal({ - enabled: true, - window: 5, - byNode: true, - byGuest: false - }); + const [scheduleGrouping, setScheduleGrouping] = createSignal(createDefaultGrouping()); - const [scheduleEscalation, setScheduleEscalation] = createSignal({ - enabled: false, - levels: [] as Array<{ after: number; notify: string }> - }); + const [scheduleEscalation, setScheduleEscalation] = createSignal(createDefaultEscalation()); // Set up destinationsRef.emailConfig function immediately destinationsRef.emailConfig = () => { @@ -1211,39 +1254,6 @@ function DestinationsTab(props: DestinationsTabProps) { // History Tab - Alert history -interface QuietHoursConfig { - enabled: boolean; - start: string; - end: string; - timezone: string; - days: Record; -} - -interface CooldownConfig { - enabled: boolean; - minutes: number; - maxAlerts: number; -} - -interface GroupingConfig { - enabled: boolean; - window: number; - maxGroupSize?: number; - byNode?: boolean; - byGuest?: boolean; -} - -interface EscalationConfig { - enabled: boolean; - timeToEscalate?: number; - levels: Array<{ - level?: number; - destinations?: string[]; - after?: number; - notify?: string; - }>; -} - // Schedule Tab - Quiet hours, cooldown, and grouping interface ScheduleTabProps { hasUnsavedChanges: () => boolean; @@ -1268,6 +1278,13 @@ function ScheduleTab(props: ScheduleTabProps) { const setGrouping = props.setGrouping; const escalation = props.escalation; const setEscalation = props.setEscalation; + const resetToDefaults = () => { + setQuietHours(createDefaultQuietHours()); + setCooldown(createDefaultCooldown()); + setGrouping(createDefaultGrouping()); + setEscalation(createDefaultEscalation()); + props.setHasUnsavedChanges(true); + }; const timezones = [ 'UTC', @@ -1295,10 +1312,23 @@ function ScheduleTab(props: ScheduleTabProps) { return (
- +
+ + +
{/* Quiet Hours */}