mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-04 14:57:05 +00:00
Update 1.2.2.2 beta
This commit is contained in:
@@ -250,6 +250,35 @@ function pathKey(path: string[]): string {
|
|||||||
return path.join("/")
|
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 ───────────────────────────────────────────────────────────────
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function HealthThresholds() {
|
export function HealthThresholds() {
|
||||||
@@ -449,10 +478,14 @@ export function HealthThresholds() {
|
|||||||
if (!leaf) return null
|
if (!leaf) return null
|
||||||
const key = pathKey(path)
|
const key = pathKey(path)
|
||||||
const val = Number(pending[key] ?? leaf.value)
|
const val = Number(pending[key] ?? leaf.value)
|
||||||
const min = leaf.min
|
|
||||||
const max = leaf.max
|
|
||||||
const step = leaf.step || 1
|
const step = leaf.step || 1
|
||||||
const unit = leaf.unit || ""
|
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 pct = ((Math.max(min, Math.min(max, val)) - min) / (max - min)) * 100
|
||||||
const custom = leaf.customised && !(key in pending)
|
const custom = leaf.customised && !(key in pending)
|
||||||
const color = severity === "critical" ? "red" : "amber"
|
const color = severity === "critical" ? "red" : "amber"
|
||||||
@@ -468,7 +501,7 @@ export function HealthThresholds() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-1 py-3">
|
<div className="px-1 py-3">
|
||||||
<div className="relative h-5 mb-1 select-none">
|
<div className="relative h-6 sm:h-5 mb-1 select-none">
|
||||||
<span
|
<span
|
||||||
className={`absolute -translate-x-1/2 text-xs font-semibold tabular-nums ${numberColor}`}
|
className={`absolute -translate-x-1/2 text-xs font-semibold tabular-nums ${numberColor}`}
|
||||||
style={{ left: `${pct}%` }}
|
style={{ left: `${pct}%` }}
|
||||||
@@ -476,7 +509,7 @@ export function HealthThresholds() {
|
|||||||
{val}{unit}
|
{val}{unit}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-6">
|
<div className="relative h-9 sm:h-6">
|
||||||
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-muted" />
|
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-muted" />
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 -translate-y-1/2 h-1.5 rounded-r-full ${fillColor}`}
|
className={`absolute top-1/2 -translate-y-1/2 h-1.5 rounded-r-full ${fillColor}`}
|
||||||
@@ -490,7 +523,7 @@ export function HealthThresholds() {
|
|||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
value={val}
|
value={val}
|
||||||
onChange={(e) => setPending((p) => ({ ...p, [key]: e.target.value }))}
|
onChange={(e) => 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}`}
|
title={`Recommended: ${leaf.recommended}${unit}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -529,11 +562,16 @@ export function HealthThresholds() {
|
|||||||
const wVal = Number(pending[wKey] ?? wLeaf.value)
|
const wVal = Number(pending[wKey] ?? wLeaf.value)
|
||||||
const cVal = Number(pending[cKey] ?? cLeaf.value)
|
const cVal = Number(pending[cKey] ?? cLeaf.value)
|
||||||
|
|
||||||
// Shared min/max from the backend leaf (both handles use the same
|
// Backend validates warning <= critical on save.
|
||||||
// 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 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 pct = (v: number) => ((Math.max(min, Math.min(max, v)) - min) / (max - min)) * 100
|
||||||
const wPct = pct(wVal)
|
const wPct = pct(wVal)
|
||||||
const cPct = pct(cVal)
|
const cPct = pct(cVal)
|
||||||
@@ -558,7 +596,7 @@ export function HealthThresholds() {
|
|||||||
they ride above the corresponding thumb regardless of the
|
they ride above the corresponding thumb regardless of the
|
||||||
slider width. Pointer events disabled so they don't steal
|
slider width. Pointer events disabled so they don't steal
|
||||||
clicks from the underlying range inputs. */}
|
clicks from the underlying range inputs. */}
|
||||||
<div className="relative h-5 mb-1 select-none">
|
<div className="relative h-6 sm:h-5 mb-1 select-none">
|
||||||
<span
|
<span
|
||||||
className={`absolute -translate-x-1/2 text-xs font-semibold tabular-nums ${wCustom ? "text-blue-400" : "text-amber-500"}`}
|
className={`absolute -translate-x-1/2 text-xs font-semibold tabular-nums ${wCustom ? "text-blue-400" : "text-amber-500"}`}
|
||||||
style={{ left: `${wPct}%` }}
|
style={{ left: `${wPct}%` }}
|
||||||
@@ -573,12 +611,9 @@ export function HealthThresholds() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track + handles. Two real <input type="range"> stacked on
|
{/* Two range inputs stacked. Mobile track box is taller so the
|
||||||
top of each other, both pointer-events:none on the bar but
|
enlarged thumbs (h-7) have room to sit without clipping. */}
|
||||||
pointer-events:auto on the thumbs (CSS in globals if you
|
<div className="relative h-9 sm:h-6">
|
||||||
want sleeker thumbs; here we use the default which already
|
|
||||||
looks decent on every browser). */}
|
|
||||||
<div className="relative h-6">
|
|
||||||
{/* Background track: OK zone (muted) running the full width */}
|
{/* Background track: OK zone (muted) running the full width */}
|
||||||
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-muted" />
|
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-muted" />
|
||||||
{/* Warn-to-Crit gradient between the two handles */}
|
{/* Warn-to-Crit gradient between the two handles */}
|
||||||
@@ -605,7 +640,7 @@ export function HealthThresholds() {
|
|||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
value={wVal}
|
value={wVal}
|
||||||
onChange={(e) => setVal(wKey, Number(e.target.value), cVal, true)}
|
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})`}
|
title={`Warning (recommended: ${wLeaf.recommended}${unit})`}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -616,7 +651,7 @@ export function HealthThresholds() {
|
|||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
value={cVal}
|
value={cVal}
|
||||||
onChange={(e) => setVal(cKey, Number(e.target.value), wVal, false)}
|
onChange={(e) => 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})`}
|
title={`Critical (recommended: ${cLeaf.recommended}${unit})`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2473,71 +2473,144 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Storage Section */}
|
{/* Storage Section — human-readable breakdown
|
||||||
<div>
|
per disk plus the raw config string in a
|
||||||
<h4 className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
collapsible details block, mirroring the
|
||||||
<HardDrive className="h-4 w-4" />
|
Network section. */}
|
||||||
Storage
|
{(() => {
|
||||||
</h4>
|
// Parse a Proxmox disk config string into
|
||||||
<div className="space-y-3">
|
// { storage, volume, path, options }
|
||||||
{vmDetails.config.rootfs && (
|
// Handles both LVM-style volumes
|
||||||
<div key="rootfs">
|
// "local-lvm:vm-101-disk-0,size=6G" and
|
||||||
<div className="text-xs text-muted-foreground mb-1">Root Filesystem</div>
|
// passthrough paths "/dev/disk/by-id/...".
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
const parseDisk = (raw: string) => {
|
||||||
{vmDetails.config.rootfs}
|
const parts = raw.split(",")
|
||||||
|
const first = parts[0] || ""
|
||||||
|
const options: Record<string, string> = {}
|
||||||
|
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 }) => (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</span>
|
||||||
|
<span className={`text-foreground ${mono ? "font-mono text-xs" : "text-sm"} ${className || ""}`}>{value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
const renderDisk = (label: string, raw: string, keyId: string) => {
|
||||||
|
const d = parseDisk(raw)
|
||||||
|
return (
|
||||||
|
<div key={keyId} className="bg-muted/30 rounded-md p-3 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<HardDrive className="h-4 w-4 text-purple-500 flex-shrink-0" />
|
||||||
|
<span className="text-sm font-semibold text-foreground">{label}</span>
|
||||||
|
{d.storage && (
|
||||||
|
<span className="text-xs text-orange-500 font-mono">
|
||||||
|
{d.storage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{d.options.size && (
|
||||||
|
<span className="text-xs text-cyan-500 font-mono">
|
||||||
|
{humanSize(d.options.size)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
|
||||||
)}
|
{d.volume && <DField label="Volume" value={d.volume} mono />}
|
||||||
{vmDetails.config.scsihw && (
|
{d.path && <DField label="Path" value={d.path} mono className="break-all" />}
|
||||||
<div key="scsihw">
|
{d.options.ssd === "1" && <DField label="Media" value="SSD" />}
|
||||||
<div className="text-xs text-muted-foreground mb-1">SCSI Controller</div>
|
{d.options.discard && <DField label="Discard" value={d.options.discard} />}
|
||||||
<div className="font-medium text-foreground">{vmDetails.config.scsihw}</div>
|
{d.options.iothread === "1" && <DField label="IOThread" value="on" />}
|
||||||
</div>
|
{d.options.cache && <DField label="Cache" value={d.options.cache} />}
|
||||||
)}
|
{d.options.aio && <DField label="AIO" value={d.options.aio} />}
|
||||||
{/* Disk Storage with proper keys */}
|
{d.options.backup === "0" && <DField label="Backup" value="excluded" className="text-red-500" />}
|
||||||
{Object.keys(vmDetails.config)
|
{d.options.backup === "1" && <DField label="Backup" value="included" />}
|
||||||
.filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/))
|
{d.options.replicate === "0" && <DField label="Replicate" value="off" />}
|
||||||
.map((diskKey) => (
|
{d.options.efitype && <DField label="EFI type" value={d.options.efitype} />}
|
||||||
<div key={`disk-${selectedVM.vmid}-${diskKey}`}>
|
{d.options.pre_enrolled_keys && <DField label="Pre-enrolled keys" value={d.options.pre_enrolled_keys} />}
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
{d.options.serial && <DField label="Serial" value={d.options.serial} mono />}
|
||||||
{diskKey.toUpperCase().replace(/(\d+)/, " $1")}
|
{d.options.mp && <DField label="Mount point" value={d.options.mp} mono />}
|
||||||
|
{d.options.acl && <DField label="ACL" value={d.options.acl} />}
|
||||||
|
</div>
|
||||||
|
<details className="text-xs">
|
||||||
|
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">Raw config</summary>
|
||||||
|
<div className="mt-1 font-mono text-foreground break-all bg-background/50 p-2 rounded">
|
||||||
|
{raw}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
</details>
|
||||||
{vmDetails.config[diskKey]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{vmDetails.config.efidisk0 && (
|
|
||||||
<div key="efidisk0">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">EFI Disk</div>
|
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
|
||||||
{vmDetails.config.efidisk0}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
{vmDetails.config.tpmstate0 && (
|
}
|
||||||
<div key="tpmstate0">
|
return (
|
||||||
<div className="text-xs text-muted-foreground mb-1">TPM State</div>
|
<div>
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
<h4 className="flex items-center gap-2 text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
||||||
{vmDetails.config.tpmstate0}
|
<HardDrive className="h-4 w-4" />
|
||||||
</div>
|
Storage
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{vmDetails.config.scsihw && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-xs uppercase tracking-wide text-muted-foreground">SCSI controller:</span>
|
||||||
|
<span className="font-mono text-foreground">{vmDetails.config.scsihw}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{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}`,
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{/* Mount Points with proper keys */}
|
)
|
||||||
{Object.keys(vmDetails.config)
|
})()}
|
||||||
.filter((key) => key.match(/^mp\d+$/))
|
|
||||||
.map((mpKey) => (
|
|
||||||
<div key={`mp-${selectedVM.vmid}-${mpKey}`}>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
|
||||||
Mount Point {mpKey.replace("mp", "")}
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
|
||||||
{vmDetails.config[mpKey]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Network Section */}
|
{/* Network Section */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -203,6 +203,34 @@ main_menu() {
|
|||||||
exec bash "$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
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
auto_repair_monitor_unit
|
auto_repair_monitor_unit
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ initialize_cache
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
security_menu() {
|
security_menu() {
|
||||||
while true; do
|
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:')"
|
menu_text+="\n$(translate 'Select an option:')"
|
||||||
|
|
||||||
local OPTION
|
local OPTION
|
||||||
|
|||||||
Reference in New Issue
Block a user