mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-31 20:44:42 +00:00
Update AppImage 1.2.1.4
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
fba0f824699660d18f77bc8558370acd725921cc34737508605c83ced3c947a4
|
821d33c23a9698cfb9b28917b7d45be0cac016f43f5db6ce3702e6109e9f0a97
|
||||||
|
|||||||
@@ -422,6 +422,12 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
|||||||
|
|
||||||
// Fetch fresh data in background (non-blocking)
|
// Fetch fresh data in background (non-blocking)
|
||||||
fetchHealthDetails().catch(() => {})
|
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) {
|
} catch (err) {
|
||||||
console.error("Error dismissing:", err)
|
console.error("Error dismissing:", err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -368,7 +368,10 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
const [activeSuppressions, setActiveSuppressions] = useState<ActiveSuppression[]>([])
|
const [activeSuppressions, setActiveSuppressions] = useState<ActiveSuppression[]>([])
|
||||||
const [loadingSuppressions, setLoadingSuppressions] = useState(true)
|
const [loadingSuppressions, setLoadingSuppressions] = useState(true)
|
||||||
const [reEnablingKey, setReEnablingKey] = useState<string | null>(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<Set<string>>(new Set())
|
||||||
|
|
||||||
// Sprint 13 / issue #195: snippets storage selector. The bash helper
|
// Sprint 13 / issue #195: snippets storage selector. The bash helper
|
||||||
// resolves it on first GPU passthrough and saves to config.json; this
|
// resolves it on first GPU passthrough and saves to config.json; this
|
||||||
@@ -419,6 +422,28 @@ export function Settings() {
|
|||||||
loadSnippetsStorage()
|
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 () => {
|
const loadProxmenuxTools = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi("/api/proxmenux/installed-tools")
|
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
|
// 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
|
// condition is still active — that surfaces in the Health Monitor, not
|
||||||
// this panel).
|
// 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
|
if (!healthEditMode) return
|
||||||
setReEnablingKey(errorKey)
|
setPendingReEnables(prev => {
|
||||||
try {
|
const next = new Set(prev)
|
||||||
await fetchApi("/api/health/un-acknowledge", {
|
if (next.has(errorKey)) {
|
||||||
method: "POST",
|
next.delete(errorKey)
|
||||||
headers: { "Content-Type": "application/json" },
|
} else {
|
||||||
body: JSON.stringify({ error_key: errorKey }),
|
next.add(errorKey)
|
||||||
})
|
}
|
||||||
setActiveSuppressions(prev => prev.filter(s => s.error_key !== errorKey))
|
return next
|
||||||
} catch (err) {
|
})
|
||||||
console.error("Failed to re-enable alert:", err)
|
|
||||||
} finally {
|
|
||||||
setReEnablingKey(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => {
|
const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => {
|
||||||
@@ -797,6 +822,7 @@ export function Settings() {
|
|||||||
setHealthEditMode(false)
|
setHealthEditMode(false)
|
||||||
setPendingChanges({})
|
setPendingChanges({})
|
||||||
setCustomValues({})
|
setCustomValues({})
|
||||||
|
setPendingReEnables(new Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveAllHealth = async () => {
|
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)
|
setHealthEditMode(false)
|
||||||
setPendingChanges({})
|
setPendingChanges({})
|
||||||
|
setPendingReEnables(new Set())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSavingAllHealth(true)
|
setSavingAllHealth(true)
|
||||||
try {
|
try {
|
||||||
await fetchApi("/api/health/settings", {
|
// 1. Persist per-category suppression duration changes (if any)
|
||||||
method: "POST",
|
if (hasPayload) {
|
||||||
headers: { "Content-Type": "application/json" },
|
await fetchApi("/api/health/settings", {
|
||||||
body: JSON.stringify(payload),
|
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
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
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({})
|
setPendingChanges({})
|
||||||
setCustomValues({})
|
setCustomValues({})
|
||||||
|
setPendingReEnables(new Set())
|
||||||
setHealthEditMode(false)
|
setHealthEditMode(false)
|
||||||
setSavedAllHealth(true)
|
setSavedAllHealth(true)
|
||||||
setTimeout(() => setSavedAllHealth(false), 3000)
|
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
|
k => pendingChanges[k] !== -2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1109,12 +1161,17 @@ export function Settings() {
|
|||||||
const dismissedAtLabel = s.acknowledged_at
|
const dismissedAtLabel = s.acknowledged_at
|
||||||
? new Date(s.acknowledged_at).toLocaleString()
|
? new Date(s.acknowledged_at).toLocaleString()
|
||||||
: ""
|
: ""
|
||||||
|
const isQueued = pendingReEnables.has(s.error_key)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={s.error_key}
|
key={s.error_key}
|
||||||
className="flex items-start sm:items-center justify-between gap-3 px-3 py-2.5 rounded-md border border-border hover:bg-muted/30 transition-colors"
|
className={`flex items-start sm:items-center justify-between gap-3 px-3 py-2.5 rounded-md border transition-colors ${
|
||||||
|
isQueued
|
||||||
|
? "border-green-500/40 bg-green-500/5"
|
||||||
|
: "border-border hover:bg-muted/30"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 min-w-0 flex-1">
|
<div className={`flex items-start gap-2 min-w-0 flex-1 ${isQueued ? "opacity-60" : ""}`}>
|
||||||
{s.permanent ? (
|
{s.permanent ? (
|
||||||
<Badge variant="outline" className="text-sm px-2 py-0.5 shrink-0 text-amber-400 border-amber-400/40 mt-0.5 font-normal">
|
<Badge variant="outline" className="text-sm px-2 py-0.5 shrink-0 text-amber-400 border-amber-400/40 mt-0.5 font-normal">
|
||||||
Permanent
|
Permanent
|
||||||
@@ -1125,7 +1182,7 @@ export function Settings() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-xs sm:text-sm font-medium text-foreground truncate" title={s.error_key}>
|
<div className={`text-xs sm:text-sm font-medium text-foreground truncate ${isQueued ? "line-through" : ""}`} title={s.error_key}>
|
||||||
{normalizeErrorKey(s.error_key)}
|
{normalizeErrorKey(s.error_key)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
<div className="text-sm text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
||||||
@@ -1138,16 +1195,22 @@ export function Settings() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-7 px-2.5 text-xs shrink-0 hover:bg-green-500/10 hover:border-green-500/50 bg-transparent"
|
className={`h-7 px-2.5 text-xs shrink-0 bg-transparent ${
|
||||||
disabled={!healthEditMode || reEnablingKey === s.error_key}
|
isQueued
|
||||||
|
? "border-green-500/50 text-green-400 hover:bg-green-500/10"
|
||||||
|
: "hover:bg-green-500/10 hover:border-green-500/50"
|
||||||
|
}`}
|
||||||
|
disabled={!healthEditMode || savingAllHealth}
|
||||||
onClick={() => handleReEnable(s.error_key)}
|
onClick={() => handleReEnable(s.error_key)}
|
||||||
title={!healthEditMode ? "Enable Health Monitor Edit mode to re-enable" : "Re-enable this alert"}
|
title={
|
||||||
|
!healthEditMode
|
||||||
|
? "Enable Health Monitor Edit mode to re-enable"
|
||||||
|
: isQueued
|
||||||
|
? "Cancel re-enable (will not be applied on Save)"
|
||||||
|
: "Queue this alert for re-enable on Save"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{reEnablingKey === s.error_key ? (
|
{isQueued ? "Undo" : "Re-enable"}
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Re-enable"
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user