diff --git a/AppImage/components/health-status-modal.tsx b/AppImage/components/health-status-modal.tsx index c6db1927..014c9b85 100644 --- a/AppImage/components/health-status-modal.tsx +++ b/AppImage/components/health-status-modal.tsx @@ -334,6 +334,8 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu if (match) return t("healthStatus.details.gatewayLatency", { latency: match[1] }) match = value.match(/^(\d+) failed login attempts in 24h$/) 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?$/) if (match) return t("healthStatus.details.uptimeDays", { count: match[1] }) match = value.match(/^(\d+) package\(s\) pending$/) diff --git a/AppImage/components/host-backup.tsx b/AppImage/components/host-backup.tsx index 3f5e5eae..827b30a7 100644 --- a/AppImage/components/host-backup.tsx +++ b/AppImage/components/host-backup.tsx @@ -51,7 +51,7 @@ import { fetchApi, getApiUrl } from "../lib/api-config" import { fetchTerminalTicket } from "../lib/terminal-ws" import { formatStorage, formatBytes } from "../lib/utils" 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 @@ -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) => { if (!iso) return null try { @@ -1314,7 +1331,7 @@ export function HostBackup() {
- {u.source} + {localizedBackendLabel(u.source, t)} {u.remote?.encrypted && ( - {archive.source} + {localizedBackendLabel(archive.source, t)} )} {remoteArc?.encrypted && ( @@ -2016,9 +2033,9 @@ function InspectModal({
{t("backup.fields.sizeLabel")} {formatBytes(localArc.size_bytes)}
{t("backup.fields.pathLabel")} {localArc.path}
{localArc.job_id &&
{t("backup.fields.jobIdLabel")} {localArc.job_id}
} - {localArc.profile &&
{t("backup.fields.profileLabel")} {localArc.profile}
} + {localArc.profile &&
{t("backup.fields.profileLabel")} {localArc.profile === "default" ? t("backup.profile.default") : localArc.profile === "custom" ? t("backup.profile.custom") : localArc.profile}
} {localArc.source_hostname &&
{t("backup.fields.sourceHostLabel")} {localArc.source_hostname}
} -
{t("backup.fields.detectedViaLabel")} {localArc.detected_via}
+
{t("backup.fields.detectedViaLabel")} {localArc.detected_via === "sidecar" ? t("backup.archives.companionFile") : localArc.detected_via}
) : null}
@@ -2779,7 +2796,7 @@ function CreateJobDialog({ onCreated: () => void editingJobId?: string | null }) { - const t = useT() + const { t, language } = useI18n() const isEdit = !!editingJobId const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1) const [jobId, setJobId] = useState("") @@ -3343,7 +3360,7 @@ function CreateJobDialog({

{isEdit ? t("backup.jobs.jobNameLocked") - : <>{t("backup.jobs.jobNameHelpBefore")} _ {t("backup.jobs.jobNameHelpAnd")} - {t("backup.jobs.jobNameHelpAfter")}} + : <>{t("backup.jobs.jobNameHelpBefore")} _ {t("backup.jobs.jobNameHelpAnd")} -. {t("backup.jobs.jobNameHelpAfter")}}

{!idValid && jobId.length > 0 && !isEdit && (

{t("backup.jobs.invalidJobName")}

@@ -3654,9 +3671,9 @@ function CreateJobDialog({ {calendarPreview.next_elapse && (
{t("backup.jobs.nextRunLabel")} - {calendarPreview.next_elapse} + {formatCalendarPreview(calendarPreview.next_elapse, language, t)} {calendarPreview.from_now && ( - ({calendarPreview.from_now}) + ({formatCalendarDistance(calendarPreview.from_now, language)}) )}
)} @@ -5774,7 +5791,7 @@ function DestinationRow({

{headline}

- {item.kind} + {localizedBackendLabel(item.kind, t)} {item.kind === "local" && item.source === "default" && ( @@ -5802,7 +5819,7 @@ function DestinationRow({ ) : ( - local + {t("backup.backends.local")} )} {/* Encryption indicator — icon-only chip for encrypted diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index 000d6740..9d395b16 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -68,6 +68,10 @@ function validatePasswordStrength(pw: string, t: (key: string) => string): strin export function Security() { const { language, t } = useI18n() const st = (key: string, params?: Record) => 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 raw = typeof message === "string" ? message : "" const normalized = raw.toLowerCase() @@ -2927,7 +2931,7 @@ ${(report.sections && report.sections.length > 0) ? ` {networkInterfaces.map((iface) => ( ))} @@ -3113,7 +3117,7 @@ ${(report.sections && report.sections.length > 0) ? ` {networkInterfaces.map((iface) => ( ))} diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index ca9f13a3..26eb8dc3 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -313,6 +313,8 @@ export function Settings() { const translated = t(key) return translated === key ? fallback : translated } + const interfaceTypeLabel = (type: string) => + tFallback(`network.interfaceTypes.${type.toLowerCase()}`, type) const [proxmenuxTools, setProxmenuxTools] = useState([]) const [updatesAvailableCount, setUpdatesAvailableCount] = useState(0) const [loadingTools, setLoadingTools] = useState(true) @@ -1005,8 +1007,8 @@ export function Settings() { - Bytes - Bits + {t("settings.networkUnits.bytes")} + {t("settings.networkUnits.bits")} @@ -1263,7 +1265,7 @@ export function Settings() {
{t("settings.healthMonitor.labels.category")}: {s.category ? tFallback(`settings.healthMonitor.categories.${s.category}`, s.category) : "—"} - {s.severity && {t("settings.healthMonitor.labels.severity")}: {tFallback(`status.${s.severity}`, s.severity)}} + {s.severity && {t("settings.healthMonitor.labels.severity")}: {tFallback(`status.${s.severity.toLowerCase()}`, s.severity)}} {dismissedAtLabel && {t("settings.healthMonitor.labels.dismissed")}: {dismissedAtLabel}}
@@ -1467,7 +1469,7 @@ export function Settings() { {iface.name} - {iface.type} + {interfaceTypeLabel(iface.type)} {isDown && !isExcluded && ( @@ -1569,9 +1571,9 @@ export function Settings() { {t("settings.snippets.title")} - {t("settings.snippets.description")} + {t("settings.snippets.description")}{" "} local - {t("settings.snippets.localNote")} + {" "}{t("settings.snippets.localNote")} diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index 04b1f76d..f4796b75 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -20,6 +20,45 @@ import { type DiskTempMap, } 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 = { + "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 { name: string 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 const isNvmeForTable = diskType === 'NVMe' - const nvmeExplanationKeys: Record = { - '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 = { 'Raw Read Error Rate': 'rawReadErrorRate', 'Write Error Rate': 'writeErrorRate', @@ -2518,7 +2525,7 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri const cleanName = name.replace(/_/g, ' ') const keyPrefix = 'storage.smartReport.attributeExplanations.' if (diskKind === 'NVMe') { - const key = nvmeExplanationKeys[cleanName] || nvmeExplanationKeys[name] + const key = getNvmeSmartAttributeKey(cleanName) return key ? t(`${keyPrefix}${key}`) : '' } if (diskKind === 'SAS') { @@ -2529,6 +2536,12 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri 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 s = (status || '').toLowerCase() 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 return ` - ${attr.name} + ${getAttrLabel(attr.name, diskType)} ${displayValue} ${attrStatusText(attr.status)} @@ -3813,6 +3826,11 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro // Extract SMART attributes from testStatus for the report 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 () => { try { @@ -4138,7 +4156,7 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro {testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
{!isNvme && !testStatus.smart_data?.is_sas &&
{attr.id}
} -
{attr.name}
+
{smartAttributeLabel(attr.name)}
{testStatus.smart_data?.is_sas ? attr.raw_value : attr.value}
{!isNvme && !testStatus.smart_data?.is_sas &&
{attr.worst}
}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index d1750501..59fe5c63 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -541,6 +541,36 @@ "footer": { "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": { "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.", @@ -1119,7 +1149,9 @@ "networkUnits": { "title": "Network Units", "description": "Change how network traffic is displayed", - "label": "Network Unit Display" + "label": "Network Unit Display", + "bytes": "Bytes", + "bits": "Bits" }, "healthMonitor": { "title": "Health Monitor", @@ -1856,26 +1888,32 @@ "counts": { "tests": { "one": "{count} test", + "few": "{count} tests", "many": "{count} tests" }, "warnings": { "one": "{count} warning", + "few": "{count} warnings", "many": "{count} warnings" }, "suggestions": { "one": "{count} suggestion", + "few": "{count} suggestions", "many": "{count} suggestions" }, "testsExecuted": { "one": "{count} test was executed.", + "few": "{count} tests were executed.", "many": "{count} tests were executed." }, "actionableWarnings": { "one": "{count} actionable warning", + "few": "{count} actionable warnings", "many": "{count} actionable warnings" }, "actionableSuggestions": { "one": "{count} actionable suggestion", + "few": "{count} actionable suggestions", "many": "{count} actionable suggestions" } }, @@ -2319,7 +2357,8 @@ "bridges": "Bridges", "bond": "Bond", "vlan": "VLAN", - "virtual": "Virtual" + "virtual": "Virtual", + "other": "Other" }, "labels": { "activeSlave": "Active slave", @@ -2805,8 +2844,9 @@ "deleteLocalTitle": "Delete local archive", "deletePbsDescription": "This removes the PBS snapshot from the datastore.", "deletePbsTitle": "Delete PBS snapshot", - "descriptionAfter": "known backups", - "descriptionBefore": "Browse and restore", + "companionFile": "companion file", + "descriptionAfter": "", + "descriptionBefore": "Browse and restore backups found in", "downloadTitle": "Download this backup", "emptyAfter": "backups yet.", "emptyBefore": "No", @@ -3113,7 +3153,7 @@ "invalidJobName": "Use only letters, numbers, dashes and underscores.", "jobNameHelpAfter": "Keep it short and readable.", "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.", "lastRun": "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", "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", - "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", "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"