Improve alerts defaults and websocket cleanup

This commit is contained in:
rcourtman
2025-09-29 13:23:35 +00:00
parent b12b4ba4ef
commit 274c7e7c12
4 changed files with 350 additions and 273 deletions
+8 -5
View File
@@ -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 {
@@ -71,10 +71,7 @@ export function ResourceTable(props: ResourceTableProps) {
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<SectionHeader title={props.title} size="sm" />
</div>
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
<style>{`
.overflow-x-auto::-webkit-scrollbar { display: none; }
`}</style>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
@@ -624,210 +624,257 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
</div>
<div class="border-t border-gray-200 dark:border-gray-700 p-4 space-y-4">
{/* Threshold table */}
{/* Threshold inputs in a responsive layout */}
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Default thresholds for all resources. Individual resources can override these values below.
<span class="ml-2 text-blue-600 dark:text-blue-400">Enter 0 or -1 to disable specific alerts.</span>
</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-2 px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Resource Type</th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">CPU<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Memory<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Storage<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">%</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk Read<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Disk Write<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Net In<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
<th class="text-center px-3 text-xs font-medium text-gray-600 dark:text-gray-400">Net Out<br/><span class="text-[10px] font-normal text-gray-500 dark:text-gray-500">MB/s</span></th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-100 dark:border-gray-700/50">
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">VMs & Containers</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.guestDefaults.cpu || 0}
title="Enter 0 or -1 to disable CPU alerts"
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.guestDefaults.memory || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.guestDefaults.disk || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="10000"
value={props.guestDefaults.diskRead || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="10000"
value={props.guestDefaults.diskWrite || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="10000"
value={props.guestDefaults.networkIn || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="10000"
value={props.guestDefaults.networkOut || 0}
onInput={(e) => {
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"
/>
</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-700/50">
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Proxmox Nodes</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.nodeDefaults.cpu || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.nodeDefaults.memory || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.nodeDefaults.disk || 0}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
</tr>
<tr>
<td class="p-1 px-2 font-medium text-gray-700 dark:text-gray-300 text-sm">Storage</td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2">
<input
type="number"
min="-1"
max="100"
value={props.storageDefault()}
onInput={(e) => {
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"
/>
</td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
<td class="text-center p-1 px-2"><span class="text-gray-400 dark:text-gray-500">-</span></td>
</tr>
</tbody>
</table>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3">
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
VMs & Containers
</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div class="space-y-1">
<label for="guest-cpu" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>CPU</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="guest-cpu"
type="number"
min="-1"
max="100"
value={props.guestDefaults.cpu ?? 0}
title="Enter 0 or -1 to disable CPU alerts"
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-memory" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Memory</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="guest-memory"
type="number"
min="-1"
max="100"
value={props.guestDefaults.memory ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-disk" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Disk</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="guest-disk"
type="number"
min="-1"
max="100"
value={props.guestDefaults.disk ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-disk-read" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Disk Read</span>
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
</label>
<input
id="guest-disk-read"
type="number"
min="-1"
max="10000"
value={props.guestDefaults.diskRead ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-disk-write" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Disk Write</span>
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
</label>
<input
id="guest-disk-write"
type="number"
min="-1"
max="10000"
value={props.guestDefaults.diskWrite ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-network-in" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Net In</span>
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
</label>
<input
id="guest-network-in"
type="number"
min="-1"
max="10000"
value={props.guestDefaults.networkIn ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="guest-network-out" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Net Out</span>
<span class="text-[10px] font-normal text-gray-400">MB/s</span>
</label>
<input
id="guest-network-out"
type="number"
min="-1"
max="10000"
value={props.guestDefaults.networkOut ?? 0}
onInput={(e) => {
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"
/>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3">
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
Proxmox Nodes
</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div class="space-y-1">
<label for="node-cpu" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>CPU</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="node-cpu"
type="number"
min="-1"
max="100"
value={props.nodeDefaults.cpu ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1">
<label for="node-memory" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Memory</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="node-memory"
type="number"
min="-1"
max="100"
value={props.nodeDefaults.memory ?? 0}
onInput={(e) => {
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"
/>
</div>
<div class="space-y-1 sm:col-span-2">
<label for="node-disk" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Disk</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="node-disk"
type="number"
min="-1"
max="100"
value={props.nodeDefaults.disk ?? 0}
onInput={(e) => {
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"
/>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 p-3 md:col-span-2">
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-3">
Storage
</h4>
<div class="grid gap-3 sm:grid-cols-3 lg:grid-cols-4">
<div class="space-y-1">
<label for="storage-usage" class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center justify-between">
<span>Usage</span>
<span class="text-[10px] font-normal text-gray-400">%</span>
</label>
<input
id="storage-usage"
type="number"
min="-1"
max="100"
value={props.storageDefault()}
onInput={(e) => {
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"
/>
</div>
</div>
</div>
</div>
</div>
{/* Alert delay settings per resource type */}
<div class="pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="mb-2">
+97 -67
View File
@@ -66,6 +66,75 @@ interface UIEmailConfig {
rateLimit: number;
}
interface QuietHoursConfig {
enabled: boolean;
start: string;
end: string;
timezone: string;
days: Record<string, boolean>;
}
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<AlertTab>('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<string, boolean>
});
const [scheduleQuietHours, setScheduleQuietHours] = createSignal<QuietHoursConfig>(createDefaultQuietHours());
const [scheduleCooldown, setScheduleCooldown] = createSignal({
enabled: true,
minutes: 30,
maxAlerts: 3
});
const [scheduleCooldown, setScheduleCooldown] = createSignal<CooldownConfig>(createDefaultCooldown());
const [scheduleGrouping, setScheduleGrouping] = createSignal({
enabled: true,
window: 5,
byNode: true,
byGuest: false
});
const [scheduleGrouping, setScheduleGrouping] = createSignal<GroupingConfig>(createDefaultGrouping());
const [scheduleEscalation, setScheduleEscalation] = createSignal({
enabled: false,
levels: [] as Array<{ after: number; notify: string }>
});
const [scheduleEscalation, setScheduleEscalation] = createSignal<EscalationConfig>(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<string, boolean>;
}
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 (
<div class="space-y-6">
<SectionHeader
label="Notification Schedule"
title="Control when alerts are allowed to fire and how they are grouped."
/>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<SectionHeader
label="Notification Schedule"
title="Control when alerts are allowed to fire and how they are grouped."
/>
<button
type="button"
onClick={resetToDefaults}
class="inline-flex items-center gap-1 self-start rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
title="Restore quiet hours, cooldown, grouping, and escalation settings to their defaults"
>
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Reset defaults
</button>
</div>
<div class="grid gap-6 lg:grid-cols-2">
{/* Quiet Hours */}