mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
fix: improve host diagnostics, storage handling, and maintenance workflows
- add zero-downtime Proxmox TLS certificate refresh from the Security panel (#307) - classify storage availability independently from missing capacity information (#309) - update ZFS ARC sizing and safely reconcile conflicting module configurations - preserve and restore migrated ZFS settings without overwriting later administrator changes - stop memory optimization from forcing the kernel overcommit policy - correlate multi-line OOM events and identify the affected LXC, cgroup limits, swap and killed process - add selectable Bash prompt path styles and clearer shell activation guidance - protect technical names during automatic translation and correct localized terminology - update the Coral and VM/LXC Apps and Updates documentation, translations and screenshots
This commit is contained in:
@@ -270,6 +270,7 @@ export function Security() {
|
||||
const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null)
|
||||
const [loadingSsl, setLoadingSsl] = useState(true)
|
||||
const [configuringSsl, setConfiguringSsl] = useState(false)
|
||||
const [reloadingSsl, setReloadingSsl] = useState(false)
|
||||
const [sslRestarting, setSslRestarting] = useState(false)
|
||||
const [showCustomCertForm, setShowCustomCertForm] = useState(false)
|
||||
const [customCertPath, setCustomCertPath] = useState("")
|
||||
@@ -1779,6 +1780,35 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
}
|
||||
}
|
||||
|
||||
const handleReloadSsl = async () => {
|
||||
setReloadingSsl(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const data = await fetchApi("/api/ssl/reload", {
|
||||
method: "POST",
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
setSslCertPath(data.cert_path || sslCertPath)
|
||||
setSslKeyPath(data.key_path || sslKeyPath)
|
||||
if (data.cert_info) {
|
||||
setProxmoxCertInfo(data.cert_info)
|
||||
}
|
||||
setSuccess(data.changed
|
||||
? st("messages.sslCertificateReloaded")
|
||||
: st("messages.sslCertificateUnchanged"))
|
||||
} else {
|
||||
setError(data.message || st("errors.reloadSslFailed"))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : st("errors.reloadSslFailed"))
|
||||
} finally {
|
||||
setReloadingSsl(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -2154,15 +2184,26 @@ ${(report.sections && report.sections.length > 0) ? `
|
||||
<p><span className="font-medium text-foreground">{st("ssl.cert")}:</span> <code className="text-xs">{sslCertPath}</code></p>
|
||||
<p><span className="font-medium text-foreground">{st("ssl.key")}:</span> <code className="text-xs">{sslKeyPath}</code></p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleDisableSsl}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={configuringSsl || sslRestarting}
|
||||
className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
||||
>
|
||||
{configuringSsl ? st("ssl.disabling") : sslRestarting ? st("ssl.restarting") : st("ssl.disableHttps")}
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleReloadSsl}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={configuringSsl || reloadingSsl || sslRestarting}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${reloadingSsl ? "animate-spin" : ""}`} />
|
||||
{reloadingSsl ? st("ssl.updatingCertificate") : st("ssl.updateCertificate")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDisableSsl}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={configuringSsl || reloadingSsl || sslRestarting}
|
||||
className="text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
||||
>
|
||||
{configuringSsl ? st("ssl.disabling") : sslRestarting ? st("ssl.restarting") : st("ssl.disableHttps")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -289,6 +289,7 @@ interface RemoteStorage {
|
||||
used: number
|
||||
available: number
|
||||
percent: number
|
||||
capacity_known?: boolean
|
||||
exclude_health: boolean
|
||||
exclude_notifications: boolean
|
||||
excluded_at?: string
|
||||
@@ -1509,7 +1510,8 @@ export function Settings() {
|
||||
const isExcluded = storage.exclude_health || storage.exclude_notifications
|
||||
const isSaving = savingStorage === storage.name
|
||||
const isNamespaceRestricted = storage.status === 'namespace_restricted'
|
||||
const isOffline = !isNamespaceRestricted && (storage.status === 'error' || storage.total === 0)
|
||||
const isOffline = !isNamespaceRestricted && storage.status !== 'active'
|
||||
const capacityKnown = storage.capacity_known ?? storage.total > 0
|
||||
|
||||
return (
|
||||
<div key={storage.name} className="grid grid-cols-[1fr_auto_auto] gap-4 py-3 items-center">
|
||||
@@ -1530,6 +1532,9 @@ export function Settings() {
|
||||
{isNamespaceRestricted && (
|
||||
<p className="text-[11px] text-blue-400 mt-0.5">{t("settings.remoteStorage.namespaceRestricted")}</p>
|
||||
)}
|
||||
{!isOffline && !isNamespaceRestricted && !capacityKnown && (
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{t("storage.capacityNotReported")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ interface ProxmoxStorage {
|
||||
used: number
|
||||
available: number
|
||||
percent: number
|
||||
capacity_known?: boolean
|
||||
node: string // Added node property for detailed debug logging
|
||||
}
|
||||
|
||||
@@ -1346,6 +1347,7 @@ export function StorageOverview() {
|
||||
// Check if storage is excluded from monitoring
|
||||
const isExcluded = storage.excluded === true
|
||||
const hasError = storage.status === "error" && !isExcluded
|
||||
const capacityKnown = storage.capacity_known ?? storage.total > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1425,46 +1427,50 @@ export function StorageOverview() {
|
||||
? t("storage.notMonitored")
|
||||
: storageStatusLabel(storage.status)}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{storage.percent}%</span>
|
||||
{capacityKnown && <span className="text-sm font-medium">{storage.percent}%</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={storage.percent}
|
||||
className={`h-2 ${
|
||||
storage.percent > 90
|
||||
? "[&>div]:bg-red-500"
|
||||
: storage.percent > 75
|
||||
? "[&>div]:bg-yellow-500"
|
||||
: "[&>div]:bg-blue-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.total")}</p>
|
||||
<p className="font-medium">{formatStorage(storage.total)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.used")}</p>
|
||||
<p
|
||||
className={`font-medium ${
|
||||
storage.percent > 90
|
||||
? "text-red-400"
|
||||
: storage.percent > 75
|
||||
? "text-yellow-400"
|
||||
: "text-blue-400"
|
||||
}`}
|
||||
>
|
||||
{formatStorage(storage.used)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.available")}</p>
|
||||
<p className="font-medium text-green-400">{formatStorage(storage.available)}</p>
|
||||
{capacityKnown ? (
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={storage.percent}
|
||||
className={`h-2 ${
|
||||
storage.percent > 90
|
||||
? "[&>div]:bg-red-500"
|
||||
: storage.percent > 75
|
||||
? "[&>div]:bg-yellow-500"
|
||||
: "[&>div]:bg-blue-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.total")}</p>
|
||||
<p className="font-medium">{formatStorage(storage.total)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.used")}</p>
|
||||
<p
|
||||
className={`font-medium ${
|
||||
storage.percent > 90
|
||||
? "text-red-400"
|
||||
: storage.percent > 75
|
||||
? "text-yellow-400"
|
||||
: "text-blue-400"
|
||||
}`}
|
||||
>
|
||||
{formatStorage(storage.used)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">{t("storage.available")}</p>
|
||||
<p className="font-medium text-green-400">{formatStorage(storage.available)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{t("storage.capacityNotReported")}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -5296,10 +5296,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mb-3 leading-relaxed">
|
||||
{t("vmLxc.updates.dockerImagesReadOnly")}
|
||||
{selectedVM.docker_inventory.checked_at
|
||||
? ` · ${t("vmLxc.updates.lastCheckedPrefix")} ${new Date(selectedVM.docker_inventory.checked_at).toLocaleString()}`
|
||||
: ""}
|
||||
{t("vmLxc.updates.lastCheckedPrefix")} {selectedVM.docker_inventory.checked_at
|
||||
? new Date(selectedVM.docker_inventory.checked_at).toLocaleString()
|
||||
: "—"}
|
||||
</div>
|
||||
{dockerInventoryRefreshing ? (
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user