diff --git a/AppImage/ProxMenux-1.2.1.4-beta.AppImage b/AppImage/ProxMenux-1.2.1.4-beta.AppImage index 04c9b6b9..75b29458 100755 Binary files a/AppImage/ProxMenux-1.2.1.4-beta.AppImage and b/AppImage/ProxMenux-1.2.1.4-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 743e7861..a8fb5213 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -fba0f824699660d18f77bc8558370acd725921cc34737508605c83ced3c947a4 +821d33c23a9698cfb9b28917b7d45be0cac016f43f5db6ce3702e6109e9f0a97 diff --git a/AppImage/components/health-status-modal.tsx b/AppImage/components/health-status-modal.tsx index 81cf78d4..63ec84a9 100644 --- a/AppImage/components/health-status-modal.tsx +++ b/AppImage/components/health-status-modal.tsx @@ -422,6 +422,12 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu // Fetch fresh data in background (non-blocking) fetchHealthDetails().catch(() => {}) + + // Notify other mounted views (e.g. Settings → Active Suppressions + // panel) that the suppression set has changed so they can refresh. + try { + window.dispatchEvent(new CustomEvent("health-suppression-changed")) + } catch {} } catch (err) { console.error("Error dismissing:", err) } finally { diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 61d5fb67..9da31e8b 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -368,7 +368,10 @@ export function Settings() { } const [activeSuppressions, setActiveSuppressions] = useState([]) const [loadingSuppressions, setLoadingSuppressions] = useState(true) - const [reEnablingKey, setReEnablingKey] = useState(null) + // Queue of error_keys the user has marked for re-enable while in Edit + // mode. The actual API calls fire on Save (alongside any dropdown + // changes); Cancel discards the queue. + const [pendingReEnables, setPendingReEnables] = useState>(new Set()) // Sprint 13 / issue #195: snippets storage selector. The bash helper // resolves it on first GPU passthrough and saves to config.json; this @@ -419,6 +422,28 @@ export function Settings() { loadSnippetsStorage() }, []) + // Refresh the Active Suppressions list whenever: + // (a) another component dispatches `health-suppression-changed` + // (e.g. the dashboard Health card after Dismiss / Re-enable), or + // (b) the user returns focus to this tab. + // Without this, dismissing an alert from the Health Monitor while + // the Settings page is mounted leaves the panel stale until full + // reload. + useEffect(() => { + const onChange = () => { loadActiveSuppressions() } + const onVisible = () => { + if (document.visibilityState === "visible") loadActiveSuppressions() + } + window.addEventListener("health-suppression-changed", onChange) + window.addEventListener("focus", onChange) + document.addEventListener("visibilitychange", onVisible) + return () => { + window.removeEventListener("health-suppression-changed", onChange) + window.removeEventListener("focus", onChange) + document.removeEventListener("visibilitychange", onVisible) + } + }, []) + const loadProxmenuxTools = async () => { try { const data = await fetchApi("/api/proxmenux/installed-tools") @@ -655,21 +680,21 @@ export function Settings() { // in sync with the server (which may have re-recorded the error if the // condition is still active — that surfaces in the Health Monitor, not // this panel). - const handleReEnable = async (errorKey: string) => { + // Toggles the error_key in the pending re-enable queue. The actual + // POST /api/health/un-acknowledge fires on Save (via + // handleSaveAllHealth), keeping the UX consistent with the + // per-category dropdowns which also defer to Save. + const handleReEnable = (errorKey: string) => { if (!healthEditMode) return - setReEnablingKey(errorKey) - try { - await fetchApi("/api/health/un-acknowledge", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ error_key: errorKey }), - }) - setActiveSuppressions(prev => prev.filter(s => s.error_key !== errorKey)) - } catch (err) { - console.error("Failed to re-enable alert:", err) - } finally { - setReEnablingKey(null) - } + setPendingReEnables(prev => { + const next = new Set(prev) + if (next.has(errorKey)) { + next.delete(errorKey) + } else { + next.add(errorKey) + } + return next + }) } const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => { @@ -797,6 +822,7 @@ export function Settings() { setHealthEditMode(false) setPendingChanges({}) setCustomValues({}) + setPendingReEnables(new Set()) } const handleSaveAllHealth = async () => { @@ -808,31 +834,57 @@ export function Settings() { } } - if (Object.keys(payload).length === 0) { + const reEnableKeys = Array.from(pendingReEnables) + const hasPayload = Object.keys(payload).length > 0 + const hasReEnables = reEnableKeys.length > 0 + + if (!hasPayload && !hasReEnables) { setHealthEditMode(false) setPendingChanges({}) + setPendingReEnables(new Set()) return } setSavingAllHealth(true) try { - await fetchApi("/api/health/settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - - // Update local state with saved values - setSuppressionCategories(prev => - prev.map(c => { - if (c.key in pendingChanges && pendingChanges[c.key] !== -2) { - return { ...c, hours: pendingChanges[c.key] } - } - return c + // 1. Persist per-category suppression duration changes (if any) + if (hasPayload) { + await fetchApi("/api/health/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), }) - ) + + setSuppressionCategories(prev => + prev.map(c => { + if (c.key in pendingChanges && pendingChanges[c.key] !== -2) { + return { ...c, hours: pendingChanges[c.key] } + } + return c + }) + ) + } + + // 2. Fire un-acknowledge for every queued re-enable (in parallel) + if (hasReEnables) { + await Promise.all( + reEnableKeys.map(errorKey => + fetchApi("/api/health/un-acknowledge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ error_key: errorKey }), + }) + ) + ) + setActiveSuppressions(prev => prev.filter(s => !pendingReEnables.has(s.error_key))) + // Notify other components (dashboard health card) that the + // suppression set changed so they can refresh. + window.dispatchEvent(new CustomEvent("health-suppression-changed")) + } + setPendingChanges({}) setCustomValues({}) + setPendingReEnables(new Set()) setHealthEditMode(false) setSavedAllHealth(true) setTimeout(() => setSavedAllHealth(false), 3000) @@ -843,7 +895,7 @@ export function Settings() { } } - const hasPendingChanges = Object.keys(pendingChanges).some( + const hasPendingChanges = pendingReEnables.size > 0 || Object.keys(pendingChanges).some( k => pendingChanges[k] !== -2 ) @@ -1109,12 +1161,17 @@ export function Settings() { const dismissedAtLabel = s.acknowledged_at ? new Date(s.acknowledged_at).toLocaleString() : "" + const isQueued = pendingReEnables.has(s.error_key) return (
-
+
{s.permanent ? ( Permanent @@ -1125,7 +1182,7 @@ export function Settings() { )}
-
+
{normalizeErrorKey(s.error_key)}
@@ -1138,16 +1195,22 @@ export function Settings() {
)