mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-06 15:56:23 +00:00
fix(monitor): localize dynamic labels and generated status text
Localize runtime labels for SMART/NVMe details, backup target badges, firewall interface types, settings units, and generated health status copy while keeping the English and Slovak catalogs in key parity.
This commit is contained in:
@@ -334,6 +334,8 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
|||||||
if (match) return t("healthStatus.details.gatewayLatency", { latency: match[1] })
|
if (match) return t("healthStatus.details.gatewayLatency", { latency: match[1] })
|
||||||
match = value.match(/^(\d+) failed login attempts in 24h$/)
|
match = value.match(/^(\d+) failed login attempts in 24h$/)
|
||||||
if (match) return t("healthStatus.details.failedLogins", { count: match[1] })
|
if (match) return t("healthStatus.details.failedLogins", { count: match[1] })
|
||||||
|
match = value.match(/^(\d+) IP\(s\) currently banned by Fail2Ban \(jails: (.+)\)$/)
|
||||||
|
if (match) return t("healthStatus.details.fail2banBannedIps", { count: match[1], jails: match[2] })
|
||||||
match = value.match(/^Uptime (\d+) days?$/)
|
match = value.match(/^Uptime (\d+) days?$/)
|
||||||
if (match) return t("healthStatus.details.uptimeDays", { count: match[1] })
|
if (match) return t("healthStatus.details.uptimeDays", { count: match[1] })
|
||||||
match = value.match(/^(\d+) package\(s\) pending$/)
|
match = value.match(/^(\d+) package\(s\) pending$/)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import { fetchApi, getApiUrl } from "../lib/api-config"
|
|||||||
import { fetchTerminalTicket } from "../lib/terminal-ws"
|
import { fetchTerminalTicket } from "../lib/terminal-ws"
|
||||||
import { formatStorage, formatBytes } from "../lib/utils"
|
import { formatStorage, formatBytes } from "../lib/utils"
|
||||||
import { getStorageUsageColor } from "../lib/storage-usage-color"
|
import { getStorageUsageColor } from "../lib/storage-usage-color"
|
||||||
import { useT } from "../lib/i18n/provider"
|
import { useI18n, useT } from "../lib/i18n/provider"
|
||||||
|
|
||||||
type TFunction = (key: string, params?: Record<string, string | number>) => string
|
type TFunction = (key: string, params?: Record<string, string | number>) => string
|
||||||
|
|
||||||
@@ -380,6 +380,23 @@ const methodBadgeCls = (m: string | undefined): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localizedBackendLabel = (source: string, t: (key: string) => string): string =>
|
||||||
|
source === "pbs" ? "PBS" : source === "borg" ? "Borg" : t("backup.backends.local")
|
||||||
|
|
||||||
|
const formatCalendarPreview = (value: string, language: string, t: (key: string) => string): string => {
|
||||||
|
if (language !== "sk") return value
|
||||||
|
const match = value.match(/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(\d{4})-(\d{2})-(\d{2})\s+(.+)$/i)
|
||||||
|
if (!match) return value
|
||||||
|
const [, weekday, year, month, day, rest] = match
|
||||||
|
return `${t(`backup.weekdays.short.${weekday.toLowerCase()}`)} ${Number(day)}. ${Number(month)}. ${year} ${rest}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCalendarDistance = (value: string, language: string): string => {
|
||||||
|
if (language !== "sk") return value
|
||||||
|
const distance = value.replace(/\s+left$/i, "").replace(/\bdays?\b/gi, "d")
|
||||||
|
return `zostáva ${distance}`
|
||||||
|
}
|
||||||
|
|
||||||
const formatRunAt = (iso: string | null) => {
|
const formatRunAt = (iso: string | null) => {
|
||||||
if (!iso) return null
|
if (!iso) return null
|
||||||
try {
|
try {
|
||||||
@@ -1314,7 +1331,7 @@ export function HostBackup() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-muted-foreground mt-0.5 flex items-center gap-3 flex-wrap">
|
<div className="text-[11px] text-muted-foreground mt-0.5 flex items-center gap-3 flex-wrap">
|
||||||
<span className={`uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded border ${sourceBadgeCls}`}>
|
<span className={`uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded border ${sourceBadgeCls}`}>
|
||||||
{u.source}
|
{localizedBackendLabel(u.source, t)}
|
||||||
</span>
|
</span>
|
||||||
{u.remote?.encrypted && (
|
{u.remote?.encrypted && (
|
||||||
<span
|
<span
|
||||||
@@ -1981,7 +1998,7 @@ function InspectModal({
|
|||||||
? "text-fuchsia-400 border-fuchsia-500/40 bg-fuchsia-500/10"
|
? "text-fuchsia-400 border-fuchsia-500/40 bg-fuchsia-500/10"
|
||||||
: "text-blue-400 border-blue-500/40 bg-blue-500/10"
|
: "text-blue-400 border-blue-500/40 bg-blue-500/10"
|
||||||
}`}>
|
}`}>
|
||||||
{archive.source}
|
{localizedBackendLabel(archive.source, t)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{remoteArc?.encrypted && (
|
{remoteArc?.encrypted && (
|
||||||
@@ -2016,9 +2033,9 @@ function InspectModal({
|
|||||||
<div><span className="text-muted-foreground">{t("backup.fields.sizeLabel")}</span> {formatBytes(localArc.size_bytes)}</div>
|
<div><span className="text-muted-foreground">{t("backup.fields.sizeLabel")}</span> {formatBytes(localArc.size_bytes)}</div>
|
||||||
<div className="sm:col-span-2"><span className="text-muted-foreground">{t("backup.fields.pathLabel")}</span> <code className="font-mono break-all">{localArc.path}</code></div>
|
<div className="sm:col-span-2"><span className="text-muted-foreground">{t("backup.fields.pathLabel")}</span> <code className="font-mono break-all">{localArc.path}</code></div>
|
||||||
{localArc.job_id && <div><span className="text-muted-foreground">{t("backup.fields.jobIdLabel")}</span> <code className="font-mono">{localArc.job_id}</code></div>}
|
{localArc.job_id && <div><span className="text-muted-foreground">{t("backup.fields.jobIdLabel")}</span> <code className="font-mono">{localArc.job_id}</code></div>}
|
||||||
{localArc.profile && <div><span className="text-muted-foreground">{t("backup.fields.profileLabel")}</span> <code className="font-mono">{localArc.profile}</code></div>}
|
{localArc.profile && <div><span className="text-muted-foreground">{t("backup.fields.profileLabel")}</span> <code className="font-mono">{localArc.profile === "default" ? t("backup.profile.default") : localArc.profile === "custom" ? t("backup.profile.custom") : localArc.profile}</code></div>}
|
||||||
{localArc.source_hostname && <div><span className="text-muted-foreground">{t("backup.fields.sourceHostLabel")}</span> <code className="font-mono">{localArc.source_hostname}</code></div>}
|
{localArc.source_hostname && <div><span className="text-muted-foreground">{t("backup.fields.sourceHostLabel")}</span> <code className="font-mono">{localArc.source_hostname}</code></div>}
|
||||||
<div><span className="text-muted-foreground">{t("backup.fields.detectedViaLabel")}</span> <code className="font-mono text-[10px]">{localArc.detected_via}</code></div>
|
<div><span className="text-muted-foreground">{t("backup.fields.detectedViaLabel")}</span> <code className="font-mono text-[10px]">{localArc.detected_via === "sidecar" ? t("backup.archives.companionFile") : localArc.detected_via}</code></div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -2779,7 +2796,7 @@ function CreateJobDialog({
|
|||||||
onCreated: () => void
|
onCreated: () => void
|
||||||
editingJobId?: string | null
|
editingJobId?: string | null
|
||||||
}) {
|
}) {
|
||||||
const t = useT()
|
const { t, language } = useI18n()
|
||||||
const isEdit = !!editingJobId
|
const isEdit = !!editingJobId
|
||||||
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1)
|
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1)
|
||||||
const [jobId, setJobId] = useState("")
|
const [jobId, setJobId] = useState("")
|
||||||
@@ -3343,7 +3360,7 @@ function CreateJobDialog({
|
|||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{isEdit
|
{isEdit
|
||||||
? t("backup.jobs.jobNameLocked")
|
? t("backup.jobs.jobNameLocked")
|
||||||
: <>{t("backup.jobs.jobNameHelpBefore")} <code className="font-mono">_</code> {t("backup.jobs.jobNameHelpAnd")} <code className="font-mono">-</code> {t("backup.jobs.jobNameHelpAfter")}</>}
|
: <>{t("backup.jobs.jobNameHelpBefore")} <code className="font-mono">_</code> {t("backup.jobs.jobNameHelpAnd")} <code className="font-mono">-</code>. {t("backup.jobs.jobNameHelpAfter")}</>}
|
||||||
</p>
|
</p>
|
||||||
{!idValid && jobId.length > 0 && !isEdit && (
|
{!idValid && jobId.length > 0 && !isEdit && (
|
||||||
<p className="text-xs text-red-500 mt-1">{t("backup.jobs.invalidJobName")}</p>
|
<p className="text-xs text-red-500 mt-1">{t("backup.jobs.invalidJobName")}</p>
|
||||||
@@ -3654,9 +3671,9 @@ function CreateJobDialog({
|
|||||||
{calendarPreview.next_elapse && (
|
{calendarPreview.next_elapse && (
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-muted-foreground">{t("backup.jobs.nextRunLabel")}</span>
|
<span className="text-muted-foreground">{t("backup.jobs.nextRunLabel")}</span>
|
||||||
<span className="text-emerald-400">{calendarPreview.next_elapse}</span>
|
<span className="text-emerald-400">{formatCalendarPreview(calendarPreview.next_elapse, language, t)}</span>
|
||||||
{calendarPreview.from_now && (
|
{calendarPreview.from_now && (
|
||||||
<span className="text-muted-foreground">({calendarPreview.from_now})</span>
|
<span className="text-muted-foreground">({formatCalendarDistance(calendarPreview.from_now, language)})</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -5774,7 +5791,7 @@ function DestinationRow({
|
|||||||
<Icon className={`h-5 w-5 flex-shrink-0 ${iconColor} mt-0.5`} />
|
<Icon className={`h-5 w-5 flex-shrink-0 ${iconColor} mt-0.5`} />
|
||||||
<h3 className="font-mono font-semibold text-sm break-all">{headline}</h3>
|
<h3 className="font-mono font-semibold text-sm break-all">{headline}</h3>
|
||||||
<Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${accent}`}>
|
<Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${accent}`}>
|
||||||
{item.kind}
|
{localizedBackendLabel(item.kind, t)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{item.kind === "local" && item.source === "default" && (
|
{item.kind === "local" && item.source === "default" && (
|
||||||
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-border text-muted-foreground">
|
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-border text-muted-foreground">
|
||||||
@@ -5802,7 +5819,7 @@ function DestinationRow({
|
|||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-amber-500/40 text-amber-400 bg-amber-500/5">
|
<Badge variant="outline" className="text-[10px] uppercase tracking-wide border-amber-500/40 text-amber-400 bg-amber-500/5">
|
||||||
local
|
{t("backup.backends.local")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{/* Encryption indicator — icon-only chip for encrypted
|
{/* Encryption indicator — icon-only chip for encrypted
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ function validatePasswordStrength(pw: string, t: (key: string) => string): strin
|
|||||||
export function Security() {
|
export function Security() {
|
||||||
const { language, t } = useI18n()
|
const { language, t } = useI18n()
|
||||||
const st = (key: string, params?: Record<string, string | number>) => t(`securityPage.${key}`, params)
|
const st = (key: string, params?: Record<string, string | number>) => t(`securityPage.${key}`, params)
|
||||||
|
const interfaceTypeLabel = (type: string) =>
|
||||||
|
["physical", "bridge", "bond", "vlan", "virtual"].includes(type)
|
||||||
|
? t(`network.interfaceTypes.${type}`)
|
||||||
|
: type
|
||||||
const authErrorText = (message: unknown, fallbackKey: string) => {
|
const authErrorText = (message: unknown, fallbackKey: string) => {
|
||||||
const raw = typeof message === "string" ? message : ""
|
const raw = typeof message === "string" ? message : ""
|
||||||
const normalized = raw.toLowerCase()
|
const normalized = raw.toLowerCase()
|
||||||
@@ -2927,7 +2931,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
<option value="">{st("firewall.anyInterface")}</option>
|
<option value="">{st("firewall.anyInterface")}</option>
|
||||||
{networkInterfaces.map((iface) => (
|
{networkInterfaces.map((iface) => (
|
||||||
<option key={iface.name} value={iface.name}>
|
<option key={iface.name} value={iface.name}>
|
||||||
{iface.name} ({iface.type}{iface.status === "up" ? `, ${st("values.up")}` : `, ${st("values.down")}`})
|
{iface.name} ({interfaceTypeLabel(iface.type)}{iface.status === "up" ? `, ${st("values.up")}` : `, ${st("values.down")}`})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -3113,7 +3117,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
<option value="">{st("firewall.any")}</option>
|
<option value="">{st("firewall.any")}</option>
|
||||||
{networkInterfaces.map((iface) => (
|
{networkInterfaces.map((iface) => (
|
||||||
<option key={iface.name} value={iface.name}>
|
<option key={iface.name} value={iface.name}>
|
||||||
{iface.name} ({iface.type})
|
{iface.name} ({interfaceTypeLabel(iface.type)})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -313,6 +313,8 @@ export function Settings() {
|
|||||||
const translated = t(key)
|
const translated = t(key)
|
||||||
return translated === key ? fallback : translated
|
return translated === key ? fallback : translated
|
||||||
}
|
}
|
||||||
|
const interfaceTypeLabel = (type: string) =>
|
||||||
|
tFallback(`network.interfaceTypes.${type.toLowerCase()}`, type)
|
||||||
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||||
const [updatesAvailableCount, setUpdatesAvailableCount] = useState(0)
|
const [updatesAvailableCount, setUpdatesAvailableCount] = useState(0)
|
||||||
const [loadingTools, setLoadingTools] = useState(true)
|
const [loadingTools, setLoadingTools] = useState(true)
|
||||||
@@ -1005,8 +1007,8 @@ export function Settings() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="Bytes">Bytes</SelectItem>
|
<SelectItem value="Bytes">{t("settings.networkUnits.bytes")}</SelectItem>
|
||||||
<SelectItem value="Bits">Bits</SelectItem>
|
<SelectItem value="Bits">{t("settings.networkUnits.bits")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1263,7 +1265,7 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
<div className="text-sm text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
||||||
<span>{t("settings.healthMonitor.labels.category")}: <span className="font-medium text-foreground/80">{s.category ? tFallback(`settings.healthMonitor.categories.${s.category}`, s.category) : "—"}</span></span>
|
<span>{t("settings.healthMonitor.labels.category")}: <span className="font-medium text-foreground/80">{s.category ? tFallback(`settings.healthMonitor.categories.${s.category}`, s.category) : "—"}</span></span>
|
||||||
{s.severity && <span>{t("settings.healthMonitor.labels.severity")}: <span className="font-medium text-foreground/80">{tFallback(`status.${s.severity}`, s.severity)}</span></span>}
|
{s.severity && <span>{t("settings.healthMonitor.labels.severity")}: <span className="font-medium text-foreground/80">{tFallback(`status.${s.severity.toLowerCase()}`, s.severity)}</span></span>}
|
||||||
{dismissedAtLabel && <span>{t("settings.healthMonitor.labels.dismissed")}: {dismissedAtLabel}</span>}
|
{dismissedAtLabel && <span>{t("settings.healthMonitor.labels.dismissed")}: {dismissedAtLabel}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1467,7 +1469,7 @@ export function Settings() {
|
|||||||
{iface.name}
|
{iface.name}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||||
{iface.type}
|
{interfaceTypeLabel(iface.type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isDown && !isExcluded && (
|
{isDown && !isExcluded && (
|
||||||
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
|
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
|
||||||
@@ -1569,9 +1571,9 @@ export function Settings() {
|
|||||||
<CardTitle>{t("settings.snippets.title")}</CardTitle>
|
<CardTitle>{t("settings.snippets.title")}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t("settings.snippets.description")}
|
{t("settings.snippets.description")}{" "}
|
||||||
<code className="mx-1">local</code>
|
<code className="mx-1">local</code>
|
||||||
{t("settings.snippets.localNote")}
|
{" "}{t("settings.snippets.localNote")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -20,6 +20,45 @@ import {
|
|||||||
type DiskTempMap,
|
type DiskTempMap,
|
||||||
} from "../lib/health-thresholds"
|
} from "../lib/health-thresholds"
|
||||||
|
|
||||||
|
// Raw smartctl names are shared by the compact SMART tab and the full
|
||||||
|
// report. Keep one canonical mapping so both views use the same labels
|
||||||
|
// and explanations instead of drifting into separate translations.
|
||||||
|
const NVME_SMART_ATTRIBUTE_KEYS: Record<string, string> = {
|
||||||
|
"Critical Warning": "criticalWarning",
|
||||||
|
"Temperature": "nvmeTemperature",
|
||||||
|
"Temperature Sensor 1": "temperatureSensor1",
|
||||||
|
"Temperature Sensor 2": "temperatureSensor2",
|
||||||
|
"Temperature Sensor 3": "temperatureSensor3",
|
||||||
|
"Available Spare": "availableSpare",
|
||||||
|
"Available Spare Threshold": "availableSpareThreshold",
|
||||||
|
"Percentage Used": "percentageUsed",
|
||||||
|
"Percent Used": "percentageUsed",
|
||||||
|
"Endurance Group Warning": "enduranceGroupWarning",
|
||||||
|
"Media Errors": "mediaErrors",
|
||||||
|
"Media and Data Integrity Errors": "mediaIntegrityErrors",
|
||||||
|
"Unsafe Shutdowns": "unsafeShutdowns",
|
||||||
|
"Power Cycles": "nvmePowerCycles",
|
||||||
|
"Power On Hours": "nvmePowerOnHours",
|
||||||
|
"Data Units Read": "dataUnitsRead",
|
||||||
|
"Data Units Written": "dataUnitsWritten",
|
||||||
|
"Host Read Commands": "hostReadCommands",
|
||||||
|
"Host Write Commands": "hostWriteCommands",
|
||||||
|
"Controller Busy Time": "controllerBusyTime",
|
||||||
|
"Error Log Entries": "errorLogEntries",
|
||||||
|
"Error Information Log Entries": "errorLogEntries",
|
||||||
|
"Warning Temp Time": "warningTempTime",
|
||||||
|
"Critical Temp Time": "criticalTempTime",
|
||||||
|
"Warning Composite Temperature Time": "warningCompositeTemperatureTime",
|
||||||
|
"Critical Composite Temperature Time": "criticalCompositeTemperatureTime",
|
||||||
|
"Thermal Management T1 Trans Count": "thermalManagementT1TransCount",
|
||||||
|
"Thermal Management T2 Trans Count": "thermalManagementT2TransCount",
|
||||||
|
"Thermal Management T1 Total Time": "thermalManagementT1TotalTime",
|
||||||
|
"Thermal Management T2 Total Time": "thermalManagementT2TotalTime",
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNvmeSmartAttributeKey = (name: string): string | undefined =>
|
||||||
|
NVME_SMART_ATTRIBUTE_KEYS[name.replace(/_/g, " ")] || NVME_SMART_ATTRIBUTE_KEYS[name]
|
||||||
|
|
||||||
interface DiskInfo {
|
interface DiskInfo {
|
||||||
name: string
|
name: string
|
||||||
size?: number // Changed from string to number (KB) for formatMemory()
|
size?: number // Changed from string to number (KB) for formatMemory()
|
||||||
@@ -2359,38 +2398,6 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
// Build attributes table - format differs for NVMe vs SATA
|
// Build attributes table - format differs for NVMe vs SATA
|
||||||
const isNvmeForTable = diskType === 'NVMe'
|
const isNvmeForTable = diskType === 'NVMe'
|
||||||
|
|
||||||
const nvmeExplanationKeys: Record<string, string> = {
|
|
||||||
'Critical Warning': 'criticalWarning',
|
|
||||||
'Temperature': 'nvmeTemperature',
|
|
||||||
'Temperature Sensor 1': 'temperatureSensor1',
|
|
||||||
'Temperature Sensor 2': 'temperatureSensor2',
|
|
||||||
'Temperature Sensor 3': 'temperatureSensor3',
|
|
||||||
'Available Spare': 'availableSpare',
|
|
||||||
'Available Spare Threshold': 'availableSpareThreshold',
|
|
||||||
'Percentage Used': 'percentageUsed',
|
|
||||||
'Percent Used': 'percentageUsed',
|
|
||||||
'Media Errors': 'mediaErrors',
|
|
||||||
'Media and Data Integrity Errors': 'mediaIntegrityErrors',
|
|
||||||
'Unsafe Shutdowns': 'unsafeShutdowns',
|
|
||||||
'Power Cycles': 'nvmePowerCycles',
|
|
||||||
'Power On Hours': 'nvmePowerOnHours',
|
|
||||||
'Data Units Read': 'dataUnitsRead',
|
|
||||||
'Data Units Written': 'dataUnitsWritten',
|
|
||||||
'Host Read Commands': 'hostReadCommands',
|
|
||||||
'Host Write Commands': 'hostWriteCommands',
|
|
||||||
'Controller Busy Time': 'controllerBusyTime',
|
|
||||||
'Error Log Entries': 'errorLogEntries',
|
|
||||||
'Error Information Log Entries': 'errorLogEntries',
|
|
||||||
'Warning Temp Time': 'warningTempTime',
|
|
||||||
'Critical Temp Time': 'criticalTempTime',
|
|
||||||
'Warning Composite Temperature Time': 'warningCompositeTemperatureTime',
|
|
||||||
'Critical Composite Temperature Time': 'criticalCompositeTemperatureTime',
|
|
||||||
'Thermal Management T1 Trans Count': 'thermalManagementT1TransCount',
|
|
||||||
'Thermal Management T2 Trans Count': 'thermalManagementT2TransCount',
|
|
||||||
'Thermal Management T1 Total Time': 'thermalManagementT1TotalTime',
|
|
||||||
'Thermal Management T2 Total Time': 'thermalManagementT2TotalTime',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sataExplanationKeys: Record<string, string> = {
|
const sataExplanationKeys: Record<string, string> = {
|
||||||
'Raw Read Error Rate': 'rawReadErrorRate',
|
'Raw Read Error Rate': 'rawReadErrorRate',
|
||||||
'Write Error Rate': 'writeErrorRate',
|
'Write Error Rate': 'writeErrorRate',
|
||||||
@@ -2518,7 +2525,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
const cleanName = name.replace(/_/g, ' ')
|
const cleanName = name.replace(/_/g, ' ')
|
||||||
const keyPrefix = 'storage.smartReport.attributeExplanations.'
|
const keyPrefix = 'storage.smartReport.attributeExplanations.'
|
||||||
if (diskKind === 'NVMe') {
|
if (diskKind === 'NVMe') {
|
||||||
const key = nvmeExplanationKeys[cleanName] || nvmeExplanationKeys[name]
|
const key = getNvmeSmartAttributeKey(cleanName)
|
||||||
return key ? t(`${keyPrefix}${key}`) : ''
|
return key ? t(`${keyPrefix}${key}`) : ''
|
||||||
}
|
}
|
||||||
if (diskKind === 'SAS') {
|
if (diskKind === 'SAS') {
|
||||||
@@ -2529,6 +2536,12 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
return key ? t(`${keyPrefix}${key}`) : ''
|
return key ? t(`${keyPrefix}${key}`) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAttrLabel = (name: string, diskKind: string): string => {
|
||||||
|
if (diskKind !== 'NVMe') return name.replace(/_/g, ' ')
|
||||||
|
const key = getNvmeSmartAttributeKey(name)
|
||||||
|
return key ? t(`storage.smartReport.attributeLabels.${key}`) : name.replace(/_/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
const attrStatusText = (status?: string) => {
|
const attrStatusText = (status?: string) => {
|
||||||
const s = (status || '').toLowerCase()
|
const s = (status || '').toLowerCase()
|
||||||
if (s === 'ok') return tSmart("statusValues.ok")
|
if (s === 'ok') return tSmart("statusValues.ok")
|
||||||
@@ -2615,7 +2628,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
|
|||||||
const displayValue = isSasDisk ? attr.raw_value : attr.value
|
const displayValue = isSasDisk ? attr.raw_value : attr.value
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col-name" style="font-weight:500;${explanation ? 'border-bottom:none;padding-bottom:2px;' : ''}">${attr.name}</td>
|
<td class="col-name" style="font-weight:500;${explanation ? 'border-bottom:none;padding-bottom:2px;' : ''}">${getAttrLabel(attr.name, diskType)}</td>
|
||||||
<td style="text-align:center;font-family:monospace;${explanation ? 'border-bottom:none;' : ''}">${displayValue}</td>
|
<td style="text-align:center;font-family:monospace;${explanation ? 'border-bottom:none;' : ''}">${displayValue}</td>
|
||||||
<td style="${explanation ? 'border-bottom:none;' : ''}"><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attrStatusText(attr.status)}</span></td>
|
<td style="${explanation ? 'border-bottom:none;' : ''}"><span class="f-tag" style="background:${statusBg};color:${statusColor}">${attrStatusText(attr.status)}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -3813,6 +3826,11 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
|
|||||||
|
|
||||||
// Extract SMART attributes from testStatus for the report
|
// Extract SMART attributes from testStatus for the report
|
||||||
const smartAttributes = testStatus.smart_data?.attributes || []
|
const smartAttributes = testStatus.smart_data?.attributes || []
|
||||||
|
const smartAttributeLabel = (name: string): string => {
|
||||||
|
if (!disk.name.startsWith("nvme")) return name.replace(/_/g, " ")
|
||||||
|
const key = getNvmeSmartAttributeKey(name)
|
||||||
|
return key ? t(`storage.smartReport.attributeLabels.${key}`) : name.replace(/_/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
const fetchSmartStatus = async () => {
|
const fetchSmartStatus = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -4138,7 +4156,7 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
|
|||||||
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
|
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
|
||||||
<div key={attr.id} className={`grid ${(isNvme || testStatus.smart_data?.is_sas) ? 'grid-cols-10' : 'grid-cols-12'} gap-2 p-3 text-sm items-center`}>
|
<div key={attr.id} className={`grid ${(isNvme || testStatus.smart_data?.is_sas) ? 'grid-cols-10' : 'grid-cols-12'} gap-2 p-3 text-sm items-center`}>
|
||||||
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-1 text-muted-foreground">{attr.id}</div>}
|
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-1 text-muted-foreground">{attr.id}</div>}
|
||||||
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-5' : 'col-span-5'} truncate`} title={attr.name}>{attr.name}</div>
|
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-5' : 'col-span-5'} truncate`} title={smartAttributeLabel(attr.name)}>{smartAttributeLabel(attr.name)}</div>
|
||||||
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-3' : 'col-span-2'} text-center font-mono`}>{testStatus.smart_data?.is_sas ? attr.raw_value : attr.value}</div>
|
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-3' : 'col-span-2'} text-center font-mono`}>{testStatus.smart_data?.is_sas ? attr.raw_value : attr.value}</div>
|
||||||
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-2 text-center font-mono text-muted-foreground">{attr.worst}</div>}
|
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-2 text-center font-mono text-muted-foreground">{attr.worst}</div>}
|
||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
|
|||||||
@@ -541,6 +541,36 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"generatedBy": "Report generated by ProxMenux Monitor"
|
"generatedBy": "Report generated by ProxMenux Monitor"
|
||||||
},
|
},
|
||||||
|
"attributeLabels": {
|
||||||
|
"criticalWarning": "Critical warning",
|
||||||
|
"nvmeTemperature": "Temperature",
|
||||||
|
"temperatureSensor1": "Temperature sensor 1",
|
||||||
|
"temperatureSensor2": "Temperature sensor 2",
|
||||||
|
"temperatureSensor3": "Temperature sensor 3",
|
||||||
|
"availableSpare": "Available spare",
|
||||||
|
"availableSpareThreshold": "Available spare threshold",
|
||||||
|
"percentageUsed": "Percentage used",
|
||||||
|
"enduranceGroupWarning": "Endurance group warning",
|
||||||
|
"mediaErrors": "Media errors",
|
||||||
|
"mediaIntegrityErrors": "Media and data integrity errors",
|
||||||
|
"unsafeShutdowns": "Unsafe shutdowns",
|
||||||
|
"nvmePowerCycles": "Power cycles",
|
||||||
|
"nvmePowerOnHours": "Power-on hours",
|
||||||
|
"dataUnitsRead": "Data units read",
|
||||||
|
"dataUnitsWritten": "Data units written",
|
||||||
|
"hostReadCommands": "Host read commands",
|
||||||
|
"hostWriteCommands": "Host write commands",
|
||||||
|
"controllerBusyTime": "Controller busy time",
|
||||||
|
"errorLogEntries": "Error log entries",
|
||||||
|
"warningTempTime": "Warning temperature time",
|
||||||
|
"criticalTempTime": "Critical temperature time",
|
||||||
|
"warningCompositeTemperatureTime": "Warning composite temperature time",
|
||||||
|
"criticalCompositeTemperatureTime": "Critical composite temperature time",
|
||||||
|
"thermalManagementT1TransCount": "Thermal management T1 transitions",
|
||||||
|
"thermalManagementT2TransCount": "Thermal management T2 transitions",
|
||||||
|
"thermalManagementT1TotalTime": "Thermal management T1 total time",
|
||||||
|
"thermalManagementT2TotalTime": "Thermal management T2 total time"
|
||||||
|
},
|
||||||
"attributeExplanations": {
|
"attributeExplanations": {
|
||||||
"criticalWarning": "Active alert flags from the NVMe controller. Any non-zero value requires immediate investigation.",
|
"criticalWarning": "Active alert flags from the NVMe controller. Any non-zero value requires immediate investigation.",
|
||||||
"nvmeTemperature": "Composite temperature reported by the controller. Sustained high temperatures cause thermal throttling and reduce NAND lifespan.",
|
"nvmeTemperature": "Composite temperature reported by the controller. Sustained high temperatures cause thermal throttling and reduce NAND lifespan.",
|
||||||
@@ -1119,7 +1149,9 @@
|
|||||||
"networkUnits": {
|
"networkUnits": {
|
||||||
"title": "Network Units",
|
"title": "Network Units",
|
||||||
"description": "Change how network traffic is displayed",
|
"description": "Change how network traffic is displayed",
|
||||||
"label": "Network Unit Display"
|
"label": "Network Unit Display",
|
||||||
|
"bytes": "Bytes",
|
||||||
|
"bits": "Bits"
|
||||||
},
|
},
|
||||||
"healthMonitor": {
|
"healthMonitor": {
|
||||||
"title": "Health Monitor",
|
"title": "Health Monitor",
|
||||||
@@ -1856,26 +1888,32 @@
|
|||||||
"counts": {
|
"counts": {
|
||||||
"tests": {
|
"tests": {
|
||||||
"one": "{count} test",
|
"one": "{count} test",
|
||||||
|
"few": "{count} tests",
|
||||||
"many": "{count} tests"
|
"many": "{count} tests"
|
||||||
},
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"one": "{count} warning",
|
"one": "{count} warning",
|
||||||
|
"few": "{count} warnings",
|
||||||
"many": "{count} warnings"
|
"many": "{count} warnings"
|
||||||
},
|
},
|
||||||
"suggestions": {
|
"suggestions": {
|
||||||
"one": "{count} suggestion",
|
"one": "{count} suggestion",
|
||||||
|
"few": "{count} suggestions",
|
||||||
"many": "{count} suggestions"
|
"many": "{count} suggestions"
|
||||||
},
|
},
|
||||||
"testsExecuted": {
|
"testsExecuted": {
|
||||||
"one": "{count} test was executed.",
|
"one": "{count} test was executed.",
|
||||||
|
"few": "{count} tests were executed.",
|
||||||
"many": "{count} tests were executed."
|
"many": "{count} tests were executed."
|
||||||
},
|
},
|
||||||
"actionableWarnings": {
|
"actionableWarnings": {
|
||||||
"one": "{count} actionable warning",
|
"one": "{count} actionable warning",
|
||||||
|
"few": "{count} actionable warnings",
|
||||||
"many": "{count} actionable warnings"
|
"many": "{count} actionable warnings"
|
||||||
},
|
},
|
||||||
"actionableSuggestions": {
|
"actionableSuggestions": {
|
||||||
"one": "{count} actionable suggestion",
|
"one": "{count} actionable suggestion",
|
||||||
|
"few": "{count} actionable suggestions",
|
||||||
"many": "{count} actionable suggestions"
|
"many": "{count} actionable suggestions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2319,7 +2357,8 @@
|
|||||||
"bridges": "Bridges",
|
"bridges": "Bridges",
|
||||||
"bond": "Bond",
|
"bond": "Bond",
|
||||||
"vlan": "VLAN",
|
"vlan": "VLAN",
|
||||||
"virtual": "Virtual"
|
"virtual": "Virtual",
|
||||||
|
"other": "Other"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"activeSlave": "Active slave",
|
"activeSlave": "Active slave",
|
||||||
@@ -2805,8 +2844,9 @@
|
|||||||
"deleteLocalTitle": "Delete local archive",
|
"deleteLocalTitle": "Delete local archive",
|
||||||
"deletePbsDescription": "This removes the PBS snapshot from the datastore.",
|
"deletePbsDescription": "This removes the PBS snapshot from the datastore.",
|
||||||
"deletePbsTitle": "Delete PBS snapshot",
|
"deletePbsTitle": "Delete PBS snapshot",
|
||||||
"descriptionAfter": "known backups",
|
"companionFile": "companion file",
|
||||||
"descriptionBefore": "Browse and restore",
|
"descriptionAfter": "",
|
||||||
|
"descriptionBefore": "Browse and restore backups found in",
|
||||||
"downloadTitle": "Download this backup",
|
"downloadTitle": "Download this backup",
|
||||||
"emptyAfter": "backups yet.",
|
"emptyAfter": "backups yet.",
|
||||||
"emptyBefore": "No",
|
"emptyBefore": "No",
|
||||||
@@ -3113,7 +3153,7 @@
|
|||||||
"invalidJobName": "Use only letters, numbers, dashes and underscores.",
|
"invalidJobName": "Use only letters, numbers, dashes and underscores.",
|
||||||
"jobNameHelpAfter": "Keep it short and readable.",
|
"jobNameHelpAfter": "Keep it short and readable.",
|
||||||
"jobNameHelpAnd": "and",
|
"jobNameHelpAnd": "and",
|
||||||
"jobNameHelpBefore": "Used in timer names, logs",
|
"jobNameHelpBefore": "Used in timer names and logs. Allowed characters also include",
|
||||||
"jobNameLocked": "Job name is locked after creation.",
|
"jobNameLocked": "Job name is locked after creation.",
|
||||||
"lastRun": "Last run",
|
"lastRun": "Last run",
|
||||||
"lastRunLabel": "Last run",
|
"lastRunLabel": "Last run",
|
||||||
@@ -3517,7 +3557,7 @@
|
|||||||
"active": "Active", "up": "UP", "kernelUpToDate": "Kernel/PVE is up to date", "proxmoxUpToDate": "Proxmox VE is up to date",
|
"active": "Active", "up": "UP", "kernelUpToDate": "Kernel/PVE is up to date", "proxmoxUpToDate": "Proxmox VE is up to date",
|
||||||
"noSecurityUpdates": "No security updates pending", "noContainerErrors": "No container startup errors", "noOomEvents": "No OOM events detected",
|
"noSecurityUpdates": "No security updates pending", "noContainerErrors": "No container startup errors", "noOomEvents": "No OOM events detected",
|
||||||
"noQmpTimeouts": "No QMP timeouts detected", "noVmFailures": "No VM startup failures", "dismissedByUser": "Dismissed by user",
|
"noQmpTimeouts": "No QMP timeouts detected", "noVmFailures": "No VM startup failures", "dismissedByUser": "Dismissed by user",
|
||||||
"gatewayLatency": "Latency to gateway: {latency} ms", "failedLogins": "{count} failed login attempts in 24h", "uptimeDays": "Uptime: {count} days",
|
"gatewayLatency": "Latency to gateway: {latency} ms", "failedLogins": "{count} failed login attempts in 24h", "fail2banBannedIps": "Fail2Ban is currently blocking {count} IP addresses (jails: {jails})", "uptimeDays": "Uptime: {count} days",
|
||||||
"pendingPackages": "{count} packages pending", "updatedDaysAgo": "Last updated {count} days ago", "storageAvailable": "{type} storage is available",
|
"pendingPackages": "{count} packages pending", "updatedDaysAgo": "Last updated {count} days ago", "storageAvailable": "{type} storage is available",
|
||||||
"mountReachable": "{type} mount is reachable", "rootfsUsed": "rootfs usage: {percent}% ({size})", "runningCtsSafe": "{count} running CTs have safe rootfs usage",
|
"mountReachable": "{type} mount is reachable", "rootfsUsed": "rootfs usage: {percent}% ({size})", "runningCtsSafe": "{count} running CTs have safe rootfs usage",
|
||||||
"pveStorageSafe": "{count} PVE block storage targets have safe usage", "remoteMountsHealthy": "{count} remote mounts are healthy"
|
"pveStorageSafe": "{count} PVE block storage targets have safe usage", "remoteMountsHealthy": "{count} remote mounts are healthy"
|
||||||
|
|||||||
Reference in New Issue
Block a user