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:
MacRimi
2026-08-25 18:48:39 +02:00
parent 2302b0967b
commit 46158209f1
48 changed files with 2449 additions and 2466 deletions
+50 -9
View File
@@ -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>
)}
+6 -1
View File
@@ -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>
+41 -35
View File
@@ -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>
)
})}
+3 -4
View File
@@ -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">