From 6b173c42b6af40958e4536b1e1d25c722719d0fd Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 1 Jul 2026 20:13:59 +0200 Subject: [PATCH] Update 1.2.2.2 beta --- AppImage/components/health-thresholds.tsx | 71 ++++++-- AppImage/components/virtual-machines.tsx | 195 +++++++++++++++------- menu | 28 ++++ scripts/menus/security_menu.sh | 8 +- 4 files changed, 222 insertions(+), 80 deletions(-) diff --git a/AppImage/components/health-thresholds.tsx b/AppImage/components/health-thresholds.tsx index e6bca8a7..935cc73c 100644 --- a/AppImage/components/health-thresholds.tsx +++ b/AppImage/components/health-thresholds.tsx @@ -250,6 +250,35 @@ function pathKey(path: string[]): string { return path.join("/") } +// Trim the visible slider range to a window around the saved + +// recommended values so the track has usable resolution (e.g. CPU +// 60–100 instead of the backend's 0–100). Derived from stable inputs +// so the range does NOT shift under an active drag. +function computeVisualRange( + values: number[], + backendMin: number, + backendMax: number, + step: number, +): { min: number; max: number } { + const totalRange = Math.max(1, backendMax - backendMin) + // Margin ≈ 25% of total range, clamped to at least 5 steps so tiny + // step sizes (e.g. step=1 on 0–100) still get a usable window. + const rawMargin = Math.max(step * 5, Math.round(totalRange * 0.25)) + const lo = Math.min(...values) + const hi = Math.max(...values) + const snap = (n: number) => Math.round(n / step) * step + let visMin = Math.max(backendMin, snap(lo - rawMargin)) + let visMax = Math.min(backendMax, snap(hi + rawMargin)) + // Ensure the window is at least 4 steps wide so the slider has + // room to move even if all inputs collapse to one value. + if (visMax - visMin < step * 4) { + const mid = (visMax + visMin) / 2 + visMin = Math.max(backendMin, snap(mid - step * 2)) + visMax = Math.min(backendMax, snap(mid + step * 2)) + } + return { min: visMin, max: visMax } +} + // ─── Component ─────────────────────────────────────────────────────────────── export function HealthThresholds() { @@ -449,10 +478,14 @@ export function HealthThresholds() { 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 { min, max } = computeVisualRange( + [leaf.value, leaf.recommended], + leaf.min, + leaf.max, + step, + ) 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" @@ -468,7 +501,7 @@ export function HealthThresholds() { return (
-
+
-
+
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}`} + 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-8 [&::-webkit-slider-thumb]:w-8 sm:[&::-webkit-slider-thumb]:h-4 sm:[&::-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-8 [&::-moz-range-thumb]:w-8 sm:[&::-moz-range-thumb]:h-4 sm:[&::-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}`} />
@@ -529,11 +562,16 @@ export function HealthThresholds() { 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) + // Backend validates warning <= critical on save. const step = Math.max(wLeaf.step, cLeaf.step) || 1 + const backendMin = Math.min(wLeaf.min, cLeaf.min) + const backendMax = Math.max(wLeaf.max, cLeaf.max) + const { min, max } = computeVisualRange( + [wLeaf.value, cLeaf.value, wLeaf.recommended, cLeaf.recommended], + backendMin, + backendMax, + step, + ) const pct = (v: number) => ((Math.max(min, Math.min(max, v)) - min) / (max - min)) * 100 const wPct = pct(wVal) const cPct = pct(cVal) @@ -558,7 +596,7 @@ export function HealthThresholds() { 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. */} -
+
- {/* 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). */} -
+ {/* Two range inputs stacked. Mobile track box is taller so the + enlarged thumbs (h-7) have room to sit without clipping. */} +
{/* Background track: OK zone (muted) running the full width */}
{/* Warn-to-Crit gradient between the two handles */} @@ -605,7 +640,7 @@ export function HealthThresholds() { disabled={!editMode} value={wVal} onChange={(e) => 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" + 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-8 [&::-webkit-slider-thumb]:w-8 sm:[&::-webkit-slider-thumb]:h-4 sm:[&::-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-8 [&::-moz-range-thumb]:w-8 sm:[&::-moz-range-thumb]:h-4 sm:[&::-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" + 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-8 [&::-webkit-slider-thumb]:w-8 sm:[&::-webkit-slider-thumb]:h-4 sm:[&::-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-8 [&::-moz-range-thumb]:w-8 sm:[&::-moz-range-thumb]:h-4 sm:[&::-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})`} />
diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 4896e6c2..a5b8f815 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -2473,71 +2473,144 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
- {/* Storage Section */} -
-

- - Storage -

-
- {vmDetails.config.rootfs && ( -
-
Root Filesystem
-
- {vmDetails.config.rootfs} + {/* Storage Section — human-readable breakdown + per disk plus the raw config string in a + collapsible details block, mirroring the + Network section. */} + {(() => { + // Parse a Proxmox disk config string into + // { storage, volume, path, options } + // Handles both LVM-style volumes + // "local-lvm:vm-101-disk-0,size=6G" and + // passthrough paths "/dev/disk/by-id/...". + const parseDisk = (raw: string) => { + const parts = raw.split(",") + const first = parts[0] || "" + const options: Record = {} + parts.slice(1).forEach((p) => { + const eq = p.indexOf("=") + if (eq > 0) { + options[p.slice(0, eq).trim()] = p.slice(eq + 1).trim() + } + }) + let storage = "", volume = "", path = "" + if (first.startsWith("/")) { + path = first + } else if (first.includes(":")) { + const [s, v] = first.split(":") + storage = s + volume = v + } else { + volume = first + } + return { storage, volume, path, options } + } + // Convert Proxmox size strings ("6G", + // "3907018584K", "40G", "4M") to a + // consistent GB/TB display. + const humanSize = (s: string): string => { + if (!s) return "" + const m = s.match(/^(\d+(?:\.\d+)?)([KMGT])?$/i) + if (!m) return s + const n = parseFloat(m[1]) + const unit = (m[2] || "").toUpperCase() + const bytes = + unit === "K" ? n * 1024 : + unit === "M" ? n * 1024 ** 2 : + unit === "G" ? n * 1024 ** 3 : + unit === "T" ? n * 1024 ** 4 : n + if (bytes >= 1024 ** 4) return `${(bytes / 1024 ** 4).toFixed(2)} TB` + if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(bytes < 10 * 1024 ** 3 ? 1 : 0)} GB` + if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(0)} MB` + return s + } + const DField = ({ label, value, mono, className }: + { label: string; value: string; mono?: boolean; className?: string }) => ( +
+ {label} + {value} +
+ ) + const renderDisk = (label: string, raw: string, keyId: string) => { + const d = parseDisk(raw) + return ( +
+
+ + {label} + {d.storage && ( + + {d.storage} + + )} + {d.options.size && ( + + {humanSize(d.options.size)} + + )}
-
- )} - {vmDetails.config.scsihw && ( -
-
SCSI Controller
-
{vmDetails.config.scsihw}
-
- )} - {/* Disk Storage with proper keys */} - {Object.keys(vmDetails.config) - .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/)) - .map((diskKey) => ( -
-
- {diskKey.toUpperCase().replace(/(\d+)/, " $1")} +
+ {d.volume && } + {d.path && } + {d.options.ssd === "1" && } + {d.options.discard && } + {d.options.iothread === "1" && } + {d.options.cache && } + {d.options.aio && } + {d.options.backup === "0" && } + {d.options.backup === "1" && } + {d.options.replicate === "0" && } + {d.options.efitype && } + {d.options.pre_enrolled_keys && } + {d.options.serial && } + {d.options.mp && } + {d.options.acl && } +
+
+ Raw config +
+ {raw}
-
- {vmDetails.config[diskKey]} -
-
- ))} - {vmDetails.config.efidisk0 && ( -
-
EFI Disk
-
- {vmDetails.config.efidisk0} -
+
- )} - {vmDetails.config.tpmstate0 && ( -
-
TPM State
-
- {vmDetails.config.tpmstate0} -
+ ) + } + return ( +
+

+ + Storage +

+
+ {vmDetails.config.scsihw && ( +
+ SCSI controller: + {vmDetails.config.scsihw} +
+ )} + {vmDetails.config.rootfs && renderDisk("Root Filesystem", vmDetails.config.rootfs as string, "rootfs")} + {Object.keys(vmDetails.config) + .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/)) + .sort() + .map((diskKey) => renderDisk( + diskKey.toUpperCase().replace(/(\d+)/, " $1"), + vmDetails.config[diskKey] as string, + `disk-${selectedVM.vmid}-${diskKey}`, + ))} + {vmDetails.config.efidisk0 && renderDisk("EFI Disk", vmDetails.config.efidisk0 as string, "efidisk0")} + {vmDetails.config.tpmstate0 && renderDisk("TPM State", vmDetails.config.tpmstate0 as string, "tpmstate0")} + {Object.keys(vmDetails.config) + .filter((key) => key.match(/^mp\d+$/)) + .sort() + .map((mpKey) => renderDisk( + `Mount Point ${mpKey.replace("mp", "")}`, + vmDetails.config[mpKey] as string, + `mp-${selectedVM.vmid}-${mpKey}`, + ))}
- )} - {/* Mount Points with proper keys */} - {Object.keys(vmDetails.config) - .filter((key) => key.match(/^mp\d+$/)) - .map((mpKey) => ( -
-
- Mount Point {mpKey.replace("mp", "")} -
-
- {vmDetails.config[mpKey]} -
-
- ))} -
-
+
+ ) + })()} {/* Network Section */}
diff --git a/menu b/menu index 2c5796be..8117082d 100644 --- a/menu +++ b/menu @@ -203,6 +203,34 @@ main_menu() { exec bash "$MAIN_MENU" } +# `menu -v` / `-h` print info and exit without opening the TUI so +# admins can query the install over SSH (issue #240). +case "${1:-}" in + -v|--version|-V) + local_ver="unknown" + [[ -f "$LOCAL_VERSION_FILE" ]] && local_ver="$(<"$LOCAL_VERSION_FILE")" + printf 'ProxMenux %s\n' "$local_ver" + if is_beta && [[ -f "$BETA_VERSION_FILE" ]]; then + printf 'Beta build: %s\n' "$(<"$BETA_VERSION_FILE")" + fi + exit 0 + ;; + -h|--help) + cat <<'EOF' +Usage: menu [OPTION] + +Interactive menu for Proxmox VE management. + +Options: + -v, --version Print installed ProxMenux version and exit. + -h, --help Show this help and exit. + +Run without arguments to launch the interactive menu. +EOF + exit 0 + ;; +esac + load_language initialize_cache auto_repair_monitor_unit diff --git a/scripts/menus/security_menu.sh b/scripts/menus/security_menu.sh index 6a27560c..f2f5c3fb 100644 --- a/scripts/menus/security_menu.sh +++ b/scripts/menus/security_menu.sh @@ -39,7 +39,13 @@ initialize_cache # ========================================================== security_menu() { while true; do - local menu_text + # `local menu_text` alone does NOT reset the variable on the + # second iteration of the while loop — bash keeps the value from + # the previous round, so `menu_text+=` accumulated a duplicate + # "Select an option:" every time the user cancelled a sub-menu + # and came back. Assigning "" on declaration guarantees a clean + # slate each round. + local menu_text="" menu_text+="\n$(translate 'Select an option:')" local OPTION