diff --git a/AppImage/ProxMenux-1.2.2.2-beta.AppImage b/AppImage/ProxMenux-1.2.2.2-beta.AppImage index 514f5cc7..e281bd6e 100755 Binary files a/AppImage/ProxMenux-1.2.2.2-beta.AppImage and b/AppImage/ProxMenux-1.2.2.2-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 8db7fb97..ba44099a 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -8b7f4694ade901c9b1058828efd32df05d6621f5648882a172b72d44d5fb6950 ProxMenux-1.2.2.2-beta.AppImage +58cf9c7ca0186d2c774e8cd58bbf0e8434ab75d4fb0d9ae580ca902549773d5d ProxMenux-1.2.2.2-beta.AppImage diff --git a/AppImage/components/health-thresholds.tsx b/AppImage/components/health-thresholds.tsx index adcba2cb..e6bca8a7 100644 --- a/AppImage/components/health-thresholds.tsx +++ b/AppImage/components/health-thresholds.tsx @@ -394,29 +394,13 @@ export function HealthThresholds() { } const renderField = (path: string[], label: string) => { + // Kept for single-value leaves that don't have a warn/crit pair + // (e.g. Memory's swap_critical). The pair-cases route to + // renderThresholdRange below. const leaf = getLeaf(tree, path) if (!leaf) return null const key = pathKey(path) const editingValue = pending[key] ?? String(leaf.value) - // Visual rules (rebuilt — the original used /40 opacity borders + - // a blue ring stacked on top of the colour border, both of which - // were nearly invisible in read-only mode and stacked weirdly when - // a value was customised): - // - // • Read-only mode (editMode=false): keep severity colour on the - // border at a higher opacity (/70 instead of /40) and on the - // background (/10) so the field is clearly readable, and - // restore foreground colour (no `opacity-70` washout). This is - // the default state the user sees most of the time — it must - // match the visual weight of the rest of the Settings page. - // • Edit mode + value matches the recommended default: severity - // border + soft severity bg, same as read-only. - // • Edit mode + value customised: ONE border in blue, replacing - // (not stacking on top of) the severity border. This is the - // single signal that "this value differs from recommended". - // - // `swap_critical` and any other `*_critical` leaf falls into the - // red bucket via the substring check. const last = path[path.length - 1] || "" const isCritical = last.toLowerCase().includes("critical") const isWarning = last.toLowerCase().includes("warning") @@ -456,6 +440,200 @@ export function HealthThresholds() { ) } + // Single-handle slider for thresholds that don't have a warn/crit + // pair (only Memory's swap_critical today). Same visual language as + // the dual-handle: red handle, value above, OK / CRIT zones below + // — so the operator doesn't read it as a different control. + const renderSingleThresholdSlider = (path: string[], severity: "warning" | "critical" = "critical") => { + const leaf = getLeaf(tree, path) + if (!leaf) return null + const key = pathKey(path) + const val = Number(pending[key] ?? leaf.value) + const min = leaf.min + const max = leaf.max + const step = leaf.step || 1 + const unit = leaf.unit || "" + const pct = ((Math.max(min, Math.min(max, val)) - min) / (max - min)) * 100 + const custom = leaf.customised && !(key in pending) + const color = severity === "critical" ? "red" : "amber" + const handleClass = severity === "critical" + ? "[&::-webkit-slider-thumb]:bg-red-500 [&::-moz-range-thumb]:bg-red-500" + : "[&::-webkit-slider-thumb]:bg-amber-500 [&::-moz-range-thumb]:bg-amber-500" + const numberColor = custom + ? "text-blue-400" + : severity === "critical" + ? "text-red-500" + : "text-amber-500" + const fillColor = severity === "critical" ? "bg-red-500/30" : "bg-amber-500/30" + + return ( +
+
+ + {val}{unit} + +
+
+
+
+ setPending((p) => ({ ...p, [key]: e.target.value }))} + className={`absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-moz-range-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:shadow [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background ${handleClass}`} + title={`Recommended: ${leaf.recommended}${unit}`} + /> +
+
+ OK < {val}{unit} + {severity === "critical" ? "CRIT" : "WARN"} > {val}{unit} +
+
+ ) + } + + // Dual-handle range slider replacing the two stacked number inputs + // for warn/crit pairs. Visual model: a horizontal track split into + // three zones — OK (left of warning, muted), WARN→CRIT (between + // handles, amber-to-red gradient), and OVER-CRIT (right of critical, + // dark red overlay). The handles themselves stay coloured (amber for + // warning, red for critical) so the operator reads the same severity + // mapping they're used to from the old inputs. Numbers ride above + // each handle and double as a click-to-edit affordance — clicking a + // number swaps it for a tight `` so precise + // values are still keyboard-friendly. Customised values surface as a + // small blue dot on the affected handle (same signal as the old blue + // ring, less visual weight). + const renderThresholdRange = ( + basePath: string[], + options?: { hideLabels?: boolean } + ) => { + const wPath = [...basePath, "warning"] + const cPath = [...basePath, "critical"] + const wLeaf = getLeaf(tree, wPath) + const cLeaf = getLeaf(tree, cPath) + if (!wLeaf || !cLeaf) return null + + const wKey = pathKey(wPath) + const cKey = pathKey(cPath) + const wVal = Number(pending[wKey] ?? wLeaf.value) + const cVal = Number(pending[cKey] ?? cLeaf.value) + + // Shared min/max from the backend leaf (both handles use the same + // range; backend validates warning <= critical on save). + const min = Math.min(wLeaf.min, cLeaf.min) + const max = Math.max(wLeaf.max, cLeaf.max) + const step = Math.max(wLeaf.step, cLeaf.step) || 1 + const pct = (v: number) => ((Math.max(min, Math.min(max, v)) - min) / (max - min)) * 100 + const wPct = pct(wVal) + const cPct = pct(cVal) + const unit = wLeaf.unit || cLeaf.unit || "" + + const wCustom = wLeaf.customised && !(wKey in pending) + const cCustom = cLeaf.customised && !(cKey in pending) + + const setVal = (key: string, value: number, peer: number, isWarn: boolean) => { + // Clamp on the fly: warning can't cross critical and vice-versa, + // matching the backend invariant so the user can't drag into an + // invalid state. + let v = value + if (isWarn && v >= peer) v = peer - step + if (!isWarn && v <= peer) v = peer + step + setPending((p) => ({ ...p, [key]: String(v) })) + } + + return ( +
+ {/* Numeric labels above each handle, positioned absolutely so + they ride above the corresponding thumb regardless of the + slider width. Pointer events disabled so they don't steal + clicks from the underlying range inputs. */} +
+ + {wVal}{unit} + + + {cVal}{unit} + +
+ + {/* Track + handles. Two real stacked on + top of each other, both pointer-events:none on the bar but + pointer-events:auto on the thumbs (CSS in globals if you + want sleeker thumbs; here we use the default which already + looks decent on every browser). */} +
+ {/* Background track: OK zone (muted) running the full width */} +
+ {/* Warn-to-Crit gradient between the two handles */} +
+ {/* OVER-CRIT zone (right of critical) — solid red dim */} +
+ {/* Two superposed range inputs. Pointer-events on the thumb + only, so the inert track bar above stays visible. */} + setVal(wKey, Number(e.target.value), cVal, true)} + className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-moz-range-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-amber-500 [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:shadow [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-amber-500 [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background" + title={`Warning (recommended: ${wLeaf.recommended}${unit})`} + /> + setVal(cKey, Number(e.target.value), wVal, false)} + className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-moz-range-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-red-500 [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:shadow [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-red-500 [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background" + title={`Critical (recommended: ${cLeaf.recommended}${unit})`} + /> +
+ + {/* Zone labels — explicit ranges so the operator knows where + "warn" starts and ends without having to read the handles. */} + {!options?.hideLabels && ( +
+ OK < {wVal}{unit} + WARN {wVal}–{cVal}{unit} + CRIT > {cVal}{unit} +
+ )} +
+ ) + } + return ( @@ -518,9 +696,9 @@ export function HealthThresholds() {
The Health Monitor and notifications fire when these thresholds are crossed. - Amber inputs are warning levels, red inputs are critical levels. A blue ring - marks a value you've customised away from the recommended default — hover the - field to see the recommendation, or use Reset to restore it. + Drag the amber handle to set the warning level and the red handle to set the + critical level. Values that differ from the recommended default appear in blue — + hover a handle to see the recommendation, or use Reset to restore it. @@ -556,7 +734,7 @@ export function HealthThresholds() {

{section.title}

- {!editMode && ( + {editMode && (