Merge develop and preserve Slovak Monitor and CLI messages

This commit is contained in:
VAIO73
2026-10-22 04:44:42 +00:00
94 changed files with 11043 additions and 1595 deletions
+45 -44
View File
@@ -48,6 +48,7 @@ import {
import { ScriptTerminalModal } from "./script-terminal-modal"
import { RestoreProgressCard } from "./restore-progress-card"
import { fetchApi, getApiUrl } from "../lib/api-config"
import { hostBackupErrorText } from "../lib/host-backup-messages"
import { fetchTerminalTicket } from "../lib/terminal-ws"
import { formatStorage, formatBytes, cancelButtonClass } from "../lib/utils"
import { getStorageUsageColor } from "../lib/storage-usage-color"
@@ -525,7 +526,7 @@ function PbsKeyfileActions({ pbsRepository }: { pbsRepository?: string }) {
await mutateInfo()
closeUpload()
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
setErr(hostBackupErrorText(e, t))
setBusy(false)
}
}
@@ -542,7 +543,7 @@ function PbsKeyfileActions({ pbsRepository }: { pbsRepository?: string }) {
await mutateInfo()
closeDelete()
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
setErr(hostBackupErrorText(e, t))
setBusy(false)
}
}
@@ -765,7 +766,7 @@ function KeyfileActionsBar({
setPass1("")
setPass2("")
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
setErr(hostBackupErrorText(e, t))
} finally {
setBusy(false)
}
@@ -956,7 +957,7 @@ export function HostBackup() {
mutateJobs()
setJobToDelete(null)
} catch (e) {
setActionError(t("backup.errors.deleteFailed", { error: `"${id}": ${e instanceof Error ? e.message : String(e)}` }))
setActionError(t("backup.errors.deleteFailed", { error: `"${id}": ${hostBackupErrorText(e, t)}` }))
} finally {
setBusyJobId(null)
}
@@ -1334,7 +1335,7 @@ export function HostBackup() {
<div className="font-medium">{t("backup.archives.remoteQueryWarning")}</div>
{remoteArchivesResp.errors.map((e, i) => (
<div key={i} className="font-mono break-all">
{e.backend}/{e.repo_name}: {e.error}
{e.backend}/{e.repo_name}: {hostBackupErrorText(e.error, t)}
</div>
))}
</div>
@@ -1761,7 +1762,7 @@ function InspectModal({
setImportFile(null)
setImportPath("")
} catch (e) {
setImportError(e instanceof Error ? e.message : String(e))
setImportError(hostBackupErrorText(e, t))
} finally {
setImporting(false)
}
@@ -1812,7 +1813,7 @@ function InspectModal({
: undefined,
})
} catch (e) {
setRestoreError(e instanceof Error ? e.message : String(e))
setRestoreError(hostBackupErrorText(e, t))
} finally {
setRestorePreparing(false)
}
@@ -1841,7 +1842,7 @@ function InspectModal({
a.click()
document.body.removeChild(a)
} catch (e) {
setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
setError(t("backup.errors.downloadFailed", { error: hostBackupErrorText(e, t) }))
} finally {
// The download itself runs in the browser's network stack;
// we just initiated it. Clear the spinner immediately.
@@ -1914,7 +1915,7 @@ function InspectModal({
document.body.removeChild(a)
setExportTask(null)
} catch (e) {
setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
setError(t("backup.errors.downloadFailed", { error: hostBackupErrorText(e, t) }))
} finally {
setDownloading(false)
}
@@ -1962,7 +1963,7 @@ function InspectModal({
setShowDeleteArchiveConfirm(false)
if (onDeleted) onDeleted(); else onClose()
} catch (e) {
setError(t("backup.errors.deleteFailed", { error: e instanceof Error ? e.message : String(e) }))
setError(t("backup.errors.deleteFailed", { error: hostBackupErrorText(e, t) }))
} finally {
setDeletingArchive(false)
}
@@ -1989,7 +1990,7 @@ function InspectModal({
) as NonNullable<typeof instructions>
setInstructions(res)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setFetchingInstructions(false)
}
@@ -2021,7 +2022,7 @@ function InspectModal({
)
setReport(res)
} catch (e: any) {
setError(e?.message || t("backup.errors.preflightFailed"))
setError(hostBackupErrorText(e, t))
} finally {
setRunning(false)
}
@@ -2164,7 +2165,7 @@ function InspectModal({
<span className="text-muted-foreground">— {exportTask.message}</span>
</div>
{exportTask.state === "failed" && exportTask.error && (
<div className="text-red-500 mt-1">{exportTask.error}</div>
<div className="text-red-500 mt-1">{hostBackupErrorText(exportTask.error, t)}</div>
)}
{exportTask.state === "completed" && exportTask.size_bytes > 0 && (
<div className="text-emerald-400">{t("backup.archives.packedSizeLabel")} {formatBytes(exportTask.size_bytes)}</div>
@@ -2706,7 +2707,7 @@ function PreflightReportView({ report }: { report: PreflightReport }) {
<Icon className={`h-3.5 w-3.5 ${color} flex-shrink-0 mt-0.5`} />
<div className="flex-1 min-w-0">
<span className={`font-mono ${color}`}>{c.id}</span>
<span className="text-muted-foreground ml-2">{c.message}</span>
<span className="text-muted-foreground ml-2">{hostBackupErrorText(c.message, t)}</span>
</div>
</div>
)
@@ -3326,9 +3327,9 @@ function CreateJobDialog({
} catch (e) {
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
const detail = err.body?.tool_output
? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
: err.message
setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
? `${hostBackupErrorText(err, t)}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
: hostBackupErrorText(err, t)
setError(t("backup.errors.encryptionSetupFailed", { error: detail || hostBackupErrorText(e, t) }))
setPbsImportBusy(false)
setSubmitting(false)
return
@@ -3391,7 +3392,7 @@ function CreateJobDialog({
})
onCreated()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setSubmitting(false)
}
@@ -3764,7 +3765,7 @@ function CreateJobDialog({
</>
) : (
<div className="text-red-500">
<span className="font-medium">{t("backup.validation.invalidLabel")}</span> {calendarPreview.error}
<span className="font-medium">{t("backup.validation.invalidLabel")}</span> {hostBackupErrorText(calendarPreview.error, t)}
</div>
)
) : (
@@ -4696,9 +4697,9 @@ function ManualBackupDialog({
} catch (e) {
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
const detail = err.body?.tool_output
? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
: err.message
setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
? `${hostBackupErrorText(err, t)}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
: hostBackupErrorText(err, t)
setError(t("backup.errors.encryptionSetupFailed", { error: detail || hostBackupErrorText(e, t) }))
setPbsImportBusy(false)
setSubmitting(false)
return
@@ -4738,7 +4739,7 @@ function ManualBackupDialog({
)
onLaunched(resp.job_id)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setSubmitting(false)
}
@@ -5542,7 +5543,7 @@ function DestinationsSection({
await fetchApi(`/api/host-backups/destinations/pbs/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
onChanged()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -5554,7 +5555,7 @@ function DestinationsSection({
await fetchApi(`/api/host-backups/destinations/borg/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
onChanged()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -5571,7 +5572,7 @@ function DestinationsSection({
)
onChanged()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -5599,7 +5600,7 @@ function DestinationsSection({
})
onChanged()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -6042,7 +6043,7 @@ function DestinationRow({
{capacity?.error && (
<p className="mt-3 text-[11px] text-muted-foreground italic">
{t("backup.destinations.capacityUnavailable", { error: capacity.error })}
{t("backup.destinations.capacityUnavailable", { error: hostBackupErrorText(capacity.error, t) })}
</p>
)}
{!capacity && (
@@ -6341,7 +6342,7 @@ function AddDestinationDialog({
)
setGeneratedKey(resp)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setGeneratingKey(false)
}
@@ -6405,7 +6406,7 @@ function AddDestinationDialog({
}
onSaved(savedRepo)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setSubmitting(false)
}
@@ -6756,7 +6757,7 @@ function UsbPicker({
mutate()
if (resp?.mountpoint) onPick(resp.mountpoint)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -6784,7 +6785,7 @@ function UsbPicker({
setFormatTyped("")
if (resp?.mountpoint) onPick(resp.mountpoint)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -7001,7 +7002,7 @@ function ExtraPathsSection() {
setNewPath("")
mutate()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setSubmitting(false)
}
@@ -7014,7 +7015,7 @@ function ExtraPathsSection() {
await fetchApi(`/api/host-backups/extra-paths?path=${encodeURIComponent(p)}`, { method: "DELETE" })
mutate()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyPath(null)
}
@@ -7180,7 +7181,7 @@ function UsbDrivesSection() {
})
mutate()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -7198,7 +7199,7 @@ function UsbDrivesSection() {
})
mutate()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -7222,7 +7223,7 @@ function UsbDrivesSection() {
setFormatTarget(null)
setFormatTyped("")
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusyKey(null)
}
@@ -7508,7 +7509,7 @@ function JobDetailModal({
setRunning(false)
setRunBaseline(null)
setRunBaselineLogPath(null)
setActionError(t("backup.errors.runFailed", { error: e instanceof Error ? e.message : String(e) }))
setActionError(t("backup.errors.runFailed", { error: hostBackupErrorText(e, t) }))
}
}
@@ -7522,7 +7523,7 @@ function JobDetailModal({
await refetch()
onChanged()
} catch (e) {
setActionError(t("backup.errors.toggleFailed", { error: e instanceof Error ? e.message : String(e) }))
setActionError(t("backup.errors.toggleFailed", { error: hostBackupErrorText(e, t) }))
} finally {
setBusy("")
}
@@ -8155,7 +8156,7 @@ function PbsKeyfileRecoveryDialog({
})
onRecovered()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
setError(hostBackupErrorText(e, t))
} finally {
setBusy(false)
}
@@ -8342,7 +8343,7 @@ function ArchiveContentsModal({
body: JSON.stringify(body),
})
.then((r: any) => setData(r))
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
.catch((e) => setError(hostBackupErrorText(e, t)))
.finally(() => setLoading(false))
}, [open, source, repo_name, snapshot, path])
@@ -8571,7 +8572,7 @@ function PlanSummary({ plan }: { plan: any }) {
<div className="text-[10px] uppercase tracking-wider text-red-400 mb-1">{t("backup.plan.blockers")}</div>
<ul className="space-y-0.5 ml-4 list-disc">
{blockers.map((b, i) => (
<li key={i} className="text-red-400">{typeof b === "string" ? b : (b.message || JSON.stringify(b))}</li>
<li key={i} className="text-red-400">{hostBackupErrorText(typeof b === "string" ? b : (b.message || JSON.stringify(b)), t)}</li>
))}
</ul>
</div>
@@ -8581,7 +8582,7 @@ function PlanSummary({ plan }: { plan: any }) {
<div className="text-[10px] uppercase tracking-wider text-amber-400 mb-1">{t("backup.plan.warnings")}</div>
<ul className="space-y-0.5 ml-4 list-disc">
{warnings.map((w, i) => (
<li key={i} className="text-amber-400">{typeof w === "string" ? w : (w.message || JSON.stringify(w))}</li>
<li key={i} className="text-amber-400">{hostBackupErrorText(typeof w === "string" ? w : (w.message || JSON.stringify(w)), t)}</li>
))}
</ul>
</div>
@@ -8611,7 +8612,7 @@ function StorageSection({ storage }: { storage: any }) {
<div className="min-w-0 flex-1">
<code className="font-mono">{e.name || e.storage || `entry ${i}`}</code>
{e.type && <span className="text-muted-foreground ml-2">{e.type}</span>}
{e.message && <div className="text-muted-foreground text-[11px]">{e.message}</div>}
{e.message && <div className="text-muted-foreground text-[11px]">{hostBackupErrorText(e.message, t)}</div>}
</div>
</li>
)
+12 -11
View File
@@ -37,6 +37,7 @@ import { fetchLxcApps, getLxcAppsCached, setLxcAppsCached, subscribeLxcApps } fr
import { categoryChipStyle, useIsLightTheme } from "../lib/category-color"
import { getCategoryLabel } from "../lib/category-label"
import { useT } from "@/lib/i18n/provider"
import { localizeAppEditorError } from "../lib/lxc-app-editor-errors"
// installed_via is optional now — an empty value means "register only,
// no version tracking, no warnings, just a clickable link". Docker
@@ -728,7 +729,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
if (lower.startsWith("registry unreachable")) {
return t("vmLxc.appEditor.upstreamErrorRegistry")
}
return msg
return localizeAppEditorError(msg, t)
}
const editorOpen = !!editing && editing.targetVmid === vmid
@@ -1212,7 +1213,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
}
const removeOne = async (appId: string) => {
if (!confirm("Remove this application from the CT's App tab?")) return
if (!confirm(t("vmLxc.appEditor.confirmRemove"))) return
setBusyAppId(appId)
setError(null)
try {
@@ -2069,14 +2070,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
id="app-package"
value={draft.package || ""}
onChange={(e) => setField({ package: e.target.value })}
placeholder={method === "dpkg" ? "e.g. jellyfin-server" : "e.g. tailscale"}
placeholder={t("vmLxc.appEditor.packageExample", { package: method === "dpkg" ? "jellyfin-server" : "tailscale" })}
/>
<div className="text-[10px] text-muted-foreground mt-1">
Auto-filled from Name. Verify with{" "}
{t("vmLxc.appEditor.packageHelpBefore")}{" "}
<code className="text-foreground/70">
{method === "dpkg" ? "dpkg -l | grep <app>" : "apk info | grep <app>"}
</code>{" "}
inside the CT.
{t("vmLxc.appEditor.packageHelpAfter")}
</div>
</div>
)}
@@ -2092,8 +2093,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
className="font-mono text-xs"
/>
<div className="text-[10px] text-muted-foreground mt-1">
Absolute path. Find it with{" "}
<code className="text-foreground/70">which &lt;app&gt;</code> or{" "}
{t("vmLxc.appEditor.binaryHelpBefore")}{" "}
<code className="text-foreground/70">which &lt;app&gt;</code>{" "}{t("vmLxc.appEditor.binaryHelpOr")}{" "}
<code className="text-foreground/70">systemctl show &lt;service&gt; -p ExecStart</code>.
</div>
</div>
@@ -2558,7 +2559,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{detectorTest.installed.version ? (
<div className="font-mono text-sm text-emerald-400">{detectorTest.installed.version}</div>
) : (
<div className="text-xs text-amber-300">{detectorTest.installed.error || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
<div className="text-xs text-amber-300">{localizeUpstreamError(detectorTest.installed.error) || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
)}
<div className="mt-1.5 text-[10px] text-muted-foreground break-all">
{detectorTest.installed.method || "—"}
@@ -2574,7 +2575,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
) : detectorTest.upstream.version ? (
<div className="font-mono text-sm text-emerald-400">{detectorTest.upstream.version}</div>
) : (
<div className="text-xs text-amber-300">{detectorTest.upstream.error || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
<div className="text-xs text-amber-300">{localizeUpstreamError(detectorTest.upstream.error) || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
)}
{detectorTest.upstream.type && (
<div className="mt-1.5 text-[10px] text-muted-foreground">{detectorTest.upstream.type}</div>
@@ -2587,7 +2588,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{error && (
<div className="text-xs text-red-400 flex items-start gap-1.5">
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
<span>{error}</span>
<span>{localizeAppEditorError(error, t)}</span>
</div>
)}
@@ -3343,7 +3344,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci, c
{error && (
<div className="text-xs text-red-400 flex items-start gap-1.5">
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
<span>{error}</span>
<span>{localizeAppEditorError(error, t)}</span>
</div>
)}
</div>
+13 -9
View File
@@ -14,6 +14,7 @@ import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { fetchApi } from "../lib/api-config"
import { monitorLinkError } from "../lib/monitor-link-errors"
import { useT } from "../lib/i18n/provider"
interface MonitorLink {
@@ -185,8 +186,9 @@ export function MonitorSwitcher({ nodeName, compact = false }: { nodeName: strin
await refreshCache("/api/monitor-links")
setOfferOrigin(false)
} catch (err) {
await refreshCache("/api/monitor-links")
setAddError(err instanceof Error ? err.message : t("monitorLinks.saveFailed"))
setAddError(monitorLinkError(err, t))
try { await refreshCache("/api/monitor-links") }
catch { /* SWR shows the load error; keep the localized mutation error. */ }
} finally { setAdding(false) }
}
const notNow = () => {
@@ -260,7 +262,7 @@ export function MonitorSwitcher({ nodeName, compact = false }: { nodeName: strin
</DropdownMenu>
{manageOpen && <ManageLinksDialog open={manageOpen} onOpenChange={setManageOpen} />}
{origin && <Dialog open={offerOrigin} onOpenChange={open => { if (!open) notNow() }}>
<DialogContent className="max-w-md" onClick={event => event.stopPropagation()}>
<DialogContent className="grid-cols-1 max-w-md" onClick={event => event.stopPropagation()}>
<DialogHeader>
<DialogTitle>{t("monitorLinks.knownTitle")}</DialogTitle>
<DialogDescription>{t("monitorLinks.knownBody", { name: origin.name })}</DialogDescription>
@@ -290,7 +292,7 @@ export function MonitorSwitcher({ nodeName, compact = false }: { nodeName: strin
function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const t = useT()
const { data, mutate } = useSWR<MonitorLinks>("/api/monitor-links?all=1", fetcher, { revalidateOnFocus: false })
const { data, mutate, error: loadError, isLoading } = useSWR<MonitorLinks>("/api/monitor-links?all=1", fetcher, { revalidateOnFocus: false })
const [form, setForm] = useState({ name: "", url: "", product: "pve" })
const [addresses, setAddresses] = useState<Record<string, string>>({})
const [editing, setEditing] = useState<string | null>(null)
@@ -304,14 +306,14 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
await mutate()
await refreshCache("/api/monitor-links")
} catch (err) {
setError(err instanceof Error ? err.message : t("monitorLinks.saveFailed"))
setError(monitorLinkError(err, t))
} finally { setBusy(false) }
}
const send = (path: string, method: string, body?: unknown) =>
fetchApi(path, { method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) })
return <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto bg-accent [&_input]:bg-background [&_textarea]:bg-background [&_[role=combobox]]:bg-background"
<DialogContent className="grid-cols-1 max-w-2xl max-h-[85vh] overflow-y-auto bg-accent [&_input]:bg-background [&_textarea]:bg-background [&_[role=combobox]]:bg-background"
onClick={event => event.stopPropagation()}>
<DialogHeader>
<DialogTitle>{t("monitorLinks.dialogTitle")}</DialogTitle>
@@ -320,7 +322,7 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
<div className="space-y-3 rounded-lg border border-border p-4">
<p className="text-sm font-medium">{t("monitorLinks.addTitle")}</p>
<div className="grid gap-3 sm:grid-cols-[1fr_1.5fr_auto]">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[1fr_1.5fr_auto]">
<div className="space-y-1.5">
<Label htmlFor="monitor-link-name">{t("monitorLinks.name")}</Label>
<Input id="monitor-link-name" value={form.name} disabled={busy} onChange={event => setForm({ ...form, name: event.target.value })} />
@@ -350,6 +352,8 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
{error && <p role="alert" className="text-sm text-red-500">{error}</p>}
<div className="space-y-3">
{isLoading && <p className="text-sm text-muted-foreground">{t("monitorLinks.checking")}</p>}
{loadError && <p role="alert" className="text-sm text-amber-500">{t("monitorLinks.loadFailed")}</p>}
{data?.links.length === 0 && <p className="text-sm text-muted-foreground">{t("monitorLinks.empty")}</p>}
{data?.links.map(link => {
const own = link.manual || link.custom_url
@@ -378,8 +382,8 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
<span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground">{t("monitorLinks.thisNode")}</span>
</div>
: isEditing
? <div className="flex gap-2">
<Input value={draft} disabled={busy} autoFocus aria-label={t("monitorLinks.address")}
? <div className="grid grid-cols-2 gap-2 sm:flex">
<Input className="col-span-2 min-w-0 sm:flex-1" value={draft} disabled={busy} autoFocus aria-label={t("monitorLinks.address")}
placeholder={link.manual ? "" : `${t("monitorLinks.autoAddress")}${shown ? `: ${shown}` : ""}`}
onChange={event => setAddresses({ ...addresses, [link.key]: event.target.value })} />
<Button size="sm" className="h-10 bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-600/50 disabled:text-white disabled:opacity-100"
+8 -5
View File
@@ -4,7 +4,8 @@ import { CalendarClock, Database } from "lucide-react"
import { Badge } from "./ui/badge"
import { Progress } from "./ui/progress"
import { formatStorage } from "../lib/utils"
import { useT } from "../lib/i18n/provider"
import { useT, useI18n } from "../lib/i18n/provider"
import { PBSProblemMessage } from "./pbs-problem-message"
export interface PBSDatastore {
name: string
@@ -13,6 +14,7 @@ export interface PBSDatastore {
used?: number | null
avail?: number | null
error?: string
error_code?: string
"backend-type"?: string
"estimated-full-date"?: number
"gc-status"?: { upid?: string | null; "removed-bytes"?: number; "pending-bytes"?: number }
@@ -42,7 +44,7 @@ export function DatastoreUsageRow({ store, compact = false }: { store: PBSDatast
<span className="font-medium text-foreground truncate">{store.name}</span>
<span className="text-xs text-muted-foreground">—</span>
</div>
<p className="text-xs text-amber-500">{store.error || t("pbs.capacityUnavailable")}</p>
<PBSProblemMessage className="text-xs text-amber-500" problem={{ code: store.error_code }} raw={store.error} fallback="pbs.capacityUnavailable" />
</div>
}
const tone = usageTone(percent)
@@ -62,10 +64,11 @@ export function DatastoreUsageRow({ store, compact = false }: { store: PBSDatast
// Same layout as the "Proxmox storage" cards of the PVE Monitor.
export function PBSDatastoreCard({ store }: { store: PBSDatastore }) {
const t = useT()
const { language } = useI18n()
const percent = datastorePercent(store)
const tone = percent == null ? null : usageTone(percent)
const fullDate = store["estimated-full-date"]
const type = store["backend-type"] || "datastore"
const type = store["backend-type"] || t("pbs.datastoreType")
return <div className={`border rounded-lg p-4 ${store.error ? "border-red-500/50 bg-red-500/5" : ""}`}>
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-2 md:gap-3 min-w-0">
@@ -99,13 +102,13 @@ export function PBSDatastoreCard({ store }: { store: PBSDatastore }) {
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">{store.error || t("storage.capacityNotReported")}</p>
<PBSProblemMessage className="text-sm text-muted-foreground" problem={{ code: store.error_code }} raw={store.error} fallback="pbs.capacityUnavailable" />
)}
{(store.path || fullDate !== undefined) && <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
{store.path && <span className="font-mono truncate" title={store.path}>{store.path}</span>}
{fullDate !== undefined && <span className="flex items-center">
<CalendarClock className="mr-1.5 h-3.5 w-3.5" />{t("pbs.estimatedFull")}:
<span className="ml-1 font-medium text-foreground">{fullDate > 0 ? new Date(fullDate * 1000).toLocaleDateString() : t("pbs.estimatedFullUnknown")}</span>
<span className="ml-1 font-medium text-foreground">{fullDate > 0 ? new Date(fullDate * 1000).toLocaleDateString(language) : t("pbs.estimatedFullUnknown")}</span>
</span>}
</div>}
</div>
+58 -32
View File
@@ -14,8 +14,10 @@ import { Label } from "./ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { PBSDatastoreCard, type PBSDatastore } from "./pbs-datastore-usage"
import { PBSProblemMessage } from "./pbs-problem-message"
import { pbsDiagnostic, pbsResultText, pbsServiceState, pbsTaskType, formatPBSDuration } from "../lib/pbs-messages"
import { fetchApi } from "../lib/api-config"
import { useT } from "../lib/i18n/provider"
import { useT, useI18n } from "../lib/i18n/provider"
interface Connection { id: string; name: string; url: string; token_id: string; fingerprint: string; has_token: boolean }
interface Detected { name: string; url: string; fingerprint: string }
@@ -26,6 +28,7 @@ interface PBSStatus {
name: string; version: string | null; fetched_at: number; services: Record<string, string>;
datastores: PBSDatastore[] | null; tasks: Task[] | null;
sync: Job[] | null; verify: Job[] | null; prune: Job[] | null; errors: Record<string, string>;
error_details?: Record<string, { code: string; params?: Record<string, unknown> }>;
}
const emptyForm = { id: "", name: "", url: "", token_id: "", fingerprint: "", token_secret: "", username: "", password: "" }
@@ -46,9 +49,10 @@ const TASK_ICONS: Record<string, typeof Activity> = {
termproxy: Terminal, dircreate: Database, "create-datastore": Database,
}
// PBS reports "OK", "WARNINGS: n" or the error text; a task without one is still running.
// PBS reports "unknown", "OK", "WARNINGS: n" or an error; no state is still running.
function taskState(task: Task): keyof typeof badgeTone {
if (!task.status) return task.endtime ? "muted" : "running"
if (task.status === "unknown") return "muted"
if (task.status === "OK") return "ok"
if (task.status.startsWith("WARNINGS")) return "warn"
return "error"
@@ -56,27 +60,19 @@ function taskState(task: Task): keyof typeof badgeTone {
// PBS reports "OK", "WARNINGS: n" or the error text of the last run.
function jobState(state?: string): keyof typeof badgeTone {
if (!state) return "muted"
if (!state || state === "unknown") return "muted"
if (state.toLowerCase() === "ok") return "ok"
if (state.toLowerCase().startsWith("warn")) return "warn"
return "error"
}
function formatDuration(seconds: number) {
if (!Number.isFinite(seconds) || seconds < 0) return "—"
if (seconds < 60) return `${Math.round(seconds)}s`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ${Math.round(seconds % 60)}s`
const hours = Math.floor(minutes / 60)
return hours < 24 ? `${hours}h ${minutes % 60}m` : `${Math.floor(hours / 24)}d ${hours % 24}h`
}
// `localOnly` is the PBS host's own Monitor: no connection picker, the
// header Refresh already reloads the page data. Its tabs show one `view` each.
export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: boolean; view?: "all" | "datastores" | "tasks" } = {}) {
const showStores = view !== "tasks"
const showTasks = view !== "datastores"
const t = useT()
const { language } = useI18n()
const { data: connections, error: connectionError, mutate: reloadConnections } = useSWR<Connections>(
"/api/pbs/servers", url => fetchApi<Connections>(url), { revalidateOnFocus: false })
const [selected, setSelected] = useState("")
@@ -85,7 +81,8 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
const [method, setMethod] = useState<Method>("password")
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState("")
const [error, setError] = useState("")
const [error, setError] = useState<unknown>(null)
const [errorFallback, setErrorFallback] = useState("pbs.saveFailed")
const { data, error: statusError, isLoading, mutate } = useSWR<PBSStatus>(
selected ? `/api/pbs/status?server=${encodeURIComponent(selected)}` : null,
url => fetchApi<PBSStatus>(url), { refreshInterval: 30000, revalidateOnFocus: false })
@@ -102,7 +99,9 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
}
const save = async () => {
setErrorFallback("pbs.saveFailed")
setBusy(true); setError(""); setMessage("")
let saved = false
try {
// A password is sent once to create a read-only token; only the token is kept.
const { username, password, ...connection } = form
@@ -113,22 +112,26 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
: await fetchApi<{ id: string }>("/api/pbs/servers", { method: "POST", body: JSON.stringify({
...connection, id: form.id || undefined, token_secret: form.token_secret || undefined,
}) })
saved = true
await reloadConnections()
setSelected(result.id)
setForm(emptyForm)
setManage(false)
} catch (err) { setError(err instanceof Error ? err.message : t("pbs.saveFailed")) }
} catch (err) { setError(saved ? { body: { code: "pbs_saved_refresh_failed" }, message: pbsDiagnostic(err) } : err) }
finally { setBusy(false) }
}
const remove = async () => {
if (!form.id || !window.confirm(t("pbs.removeConfirm"))) return
setErrorFallback("pbs.removeFailed")
setBusy(true); setError(""); setMessage("")
let removed = false
try {
await fetchApi(`/api/pbs/servers/${encodeURIComponent(form.id)}`, { method: "DELETE" })
removed = true
setForm(emptyForm)
await reloadConnections()
} catch (err) { setError(err instanceof Error ? err.message : t("pbs.saveFailed")) }
} catch (err) { setError(removed ? { body: { code: "pbs_removed_refresh_failed" }, message: pbsDiagnostic(err) } : err) }
finally { setBusy(false) }
}
const now = Date.now() / 1000
@@ -150,8 +153,9 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
<Settings2 className="mr-2 h-4 w-4" />{t("pbs.manage")}
</Button>
</div>}
{(connectionError || statusError) && <p role="alert" className="text-sm text-red-500">
{String((connectionError || statusError)?.message || t("pbs.loadFailed"))}</p>}
{(connectionError || statusError) && <div role="alert" className="text-sm text-red-500">
<PBSProblemMessage problem={connectionError || statusError} raw={pbsDiagnostic(connectionError || statusError)} />
</div>}
{isLoading && <p role="status" className="text-sm text-muted-foreground">{t("pbs.loading")}</p>}
{!selected && !connectionError && connections && <p className="text-sm text-muted-foreground">{t("pbs.noServer")}</p>}
{!localOnly && !manage && (connections?.detected?.length || 0) > 0 && <Card className="bg-card border-border">
@@ -171,7 +175,7 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
</CardContent>
</Card>}
{!localOnly && <Dialog open={manage} onOpenChange={open => { if (!busy) setManage(open) }}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto bg-accent [&_input]:bg-background [&_[role=combobox]]:bg-background">
<DialogContent className="grid-cols-1 max-w-2xl max-h-[85vh] overflow-y-auto bg-accent [&_input]:bg-background [&_[role=combobox]]:bg-background">
<DialogHeader>
<DialogTitle>{t("pbs.manage")}</DialogTitle>
<DialogDescription>{method === "password" && !form.id ? t("pbs.passwordHelp") : t("pbs.permissionsHelp")}</DialogDescription>
@@ -201,19 +205,26 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
</Button>)}
</div>
</div>}
<div className="grid gap-4 md:grid-cols-2">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{(["name", "url", ...(method === "password" && !form.id ? ["username", "password"] : ["token_id", "token_secret"]), "fingerprint"] as const).map(field => {
const secret = field === "token_secret" || field === "password"
return <div key={field} className="space-y-2">
<Label htmlFor={`pbs-${field}`}>{t(`pbs.${field}`)}</Label>
const help = field === "url" ? "pbs.urlHelp" : field === "username" ? "pbs.usernameHelp"
: field === "token_id" ? "pbs.tokenIdHelp" : field === "token_secret"
? form.id ? "pbs.tokenSecretKeepHelp" : "pbs.tokenSecretHelp" : undefined
return <div key={field} className="min-w-0 space-y-2">
<Label htmlFor={`pbs-${field}`} className="block leading-5">{t(`pbs.${field}`)}</Label>
<Input id={`pbs-${field}`} disabled={busy} type={secret ? "password" : field === "url" ? "url" : "text"}
aria-describedby={help ? `pbs-${field}-help` : undefined}
autoComplete={secret ? "new-password" : "off"} placeholder={field === "username" ? "root@pam" : undefined}
value={form[field as keyof typeof form]} onChange={event => setForm({ ...form, [field]: event.target.value })} />
{help && <p id={`pbs-${field}-help`} className="text-xs text-muted-foreground">{t(help)}</p>}
</div>
})}
</div>
<p className="text-xs text-muted-foreground">{t("pbs.tlsHelp")}</p>
{error && <p role="alert" className="text-sm text-red-500">{error}</p>}
{!!error && <div role="alert" className="text-sm text-red-500">
<PBSProblemMessage problem={error} raw={pbsDiagnostic(error)} fallback={errorFallback} />
</div>}
{message && <p role="status" className="text-sm text-green-500">{message}</p>}
<div className="flex flex-wrap justify-end gap-2">
{form.id && <Button variant="outline" disabled={busy} className="mr-auto text-red-500 hover:text-red-500" onClick={remove}>
@@ -230,14 +241,17 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
</Dialog>}
{data && <>
{!localOnly && <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>{data.name} · PBS {data.version || "—"} · {t("pbs.updatedAt", { time: new Date(data.fetched_at * 1000).toLocaleTimeString() })}</span>
<span>{data.name} · PBS {data.version || "—"} · {t("pbs.updatedAt", { time: new Date(data.fetched_at * 1000).toLocaleTimeString(language) })}</span>
{Object.keys(data.services).length > 0 && Object.values(data.services).every(state => state === "active")
? <Badge variant="outline" className={badgeTone.ok}>{t("pbs.servicesActive")}</Badge>
: Object.entries(data.services).filter(([, state]) => state !== "active").map(([service, state]) =>
<Badge key={service} variant="outline" className={badgeTone.error}>{service}: {state}</Badge>)}
<Badge key={service} variant="outline" className={badgeTone.error}>{service}: {pbsServiceState(state, t)}</Badge>)}
</div>}
{Object.entries(data.errors).filter(([section]) => section === "datastores" ? showStores : showTasks)
.map(([section, detail]) => <p key={section} role="alert" className="text-sm text-amber-500">{t(`pbs.${section}`)}: {detail}</p>)}
.map(([section, detail]) => <div key={section} role="alert" className="text-sm text-amber-500">
<p className="font-medium">{t(`pbs.${["datastores", "tasks", "sync", "verify", "prune"].includes(section) ? section : "title"}`)}</p>
<PBSProblemMessage problem={data.error_details?.[section]} raw={detail} />
</div>)}
{showStores && <Card className="bg-card border-border">
<CardHeader><CardTitle className="flex items-center text-foreground"><Database className="mr-2 h-5 w-5" />{t("pbs.datastores")}</CardTitle></CardHeader>
@@ -268,14 +282,14 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
return <tr key={row.upid} className="border-b border-border/60 last:border-0">
<td className="py-2 pr-3"><span className="flex items-center gap-2">
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="text-foreground">{row.worker_type}</span>
<span className="text-foreground" title={row.worker_type}>{pbsTaskType(row.worker_type, t)}</span>
{row.worker_id && <span className="truncate text-muted-foreground">{row.worker_id}</span>}
</span></td>
<td className="whitespace-nowrap py-2 pr-3 tabular-nums text-muted-foreground">{row.starttime ? new Date(row.starttime * 1000).toLocaleString() : "—"}</td>
<td className="whitespace-nowrap py-2 pr-3 tabular-nums text-muted-foreground">{row.starttime && end ? formatDuration(end - row.starttime) : "—"}</td>
<td className="py-2"><Badge variant="outline" className={`${badgeTone[state]} max-w-[16rem] truncate`} title={row.status}>
{state === "running" ? t("pbs.running") : row.status || "—"}
</Badge></td>
<td className="whitespace-nowrap py-2 pr-3 tabular-nums text-muted-foreground">{row.starttime ? new Date(row.starttime * 1000).toLocaleString(language) : "—"}</td>
<td className="whitespace-nowrap py-2 pr-3 tabular-nums text-muted-foreground">{row.starttime && end ? formatPBSDuration(end - row.starttime, t) : "—"}</td>
<td className="py-2">
<PBSResultBadge status={row.status} running={state === "running"} tone={badgeTone[state]} />
</td>
</tr>
})}</tbody>
</table></div>}
@@ -299,7 +313,7 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
<td className="py-2 pr-3"><span className="text-foreground">{t(`pbs.${kind}`)}</span> <span className="text-muted-foreground">· {row.id}</span></td>
<td className="py-2 pr-3 text-muted-foreground">{row.store || "—"}</td>
<td className="py-2 pr-3 font-mono text-xs text-muted-foreground">{row.schedule || "—"}</td>
<td className="py-2"><Badge variant="outline" className={badgeTone[jobState(row["last-run-state"])]}>{row["last-run-state"] || "—"}</Badge></td>
<td className="py-2"><PBSResultBadge status={row["last-run-state"]} tone={badgeTone[jobState(row["last-run-state"])]} /></td>
</tr>)}</tbody>
</table></div>
})()}
@@ -309,3 +323,15 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
</>}
</div>
}
function PBSResultBadge({ status, running = false, tone }: { status?: string; running?: boolean; tone: string }) {
const t = useT()
const badge = <Badge variant="outline" className={`${tone} max-w-[16rem] truncate`}>
{running ? t("pbs.running") : pbsResultText(status, t)}
</Badge>
if (running || !status || status === "unknown" || /^ok$/i.test(status) || /^WARNINGS:\s*\d+\s*$/i.test(status)) return badge
return <details className="max-w-xs">
<summary className="cursor-pointer" aria-label={t("pbs.technicalDetails")}>{badge}</summary>
<pre className="mt-1 whitespace-pre-wrap break-words text-xs text-muted-foreground">{status}</pre>
</details>
}
@@ -0,0 +1,19 @@
"use client"
import { pbsErrorText, pbsIsKnownError } from "../lib/pbs-messages"
import { useT } from "../lib/i18n/provider"
// Known application messages are already explained in the active language.
// Preserve unrecognized external diagnostics as escaped technical details.
export function PBSProblemMessage({ problem, raw, fallback, className = "" }: {
problem?: unknown; raw?: string; fallback?: string; className?: string
}) {
const t = useT()
return <div className={`min-w-0 space-y-1 ${className}`}>
<p>{pbsErrorText(problem, t, fallback)}</p>
{raw && !pbsIsKnownError(problem) && <details className="text-xs">
<summary className="cursor-pointer">{t("pbs.technicalDetails")}</summary>
<pre className="mt-1 whitespace-pre-wrap break-words font-mono">{raw}</pre>
</details>}
</div>
}
+22 -10
View File
@@ -31,6 +31,7 @@ import {
import "xterm/css/xterm.css"
import { API_PORT } from "@/lib/api-config"
import { getTicketedWsUrl } from "@/lib/terminal-ws"
import { scriptCompletion, scriptCompletionOutput } from "@/lib/script-completion"
import { useT } from "../lib/i18n/provider"
interface WebInteraction {
@@ -52,6 +53,7 @@ interface ScriptTerminalModalProps {
params?: Record<string, string>
completedSuccessfullyMessage?: string
completedWithErrorMessage?: (exitCode: number) => string
cancelledExitCode?: number
// Optional callback fired when the script's WebSocket closes
// (script_runner sends an exit code and then closes). Lets the
// parent auto-dismiss the modal — used by host-backup's Restore
@@ -69,6 +71,7 @@ export function ScriptTerminalModal({
params = { EXECUTION_MODE: "web" },
completedSuccessfullyMessage,
completedWithErrorMessage,
cancelledExitCode,
onComplete,
}: ScriptTerminalModalProps) {
const t = useT()
@@ -103,10 +106,12 @@ export function ScriptTerminalModal({
const getCompletionMessage = useCallback(
(exitCode: number) =>
exitCode === 0
scriptCompletion(exitCode, cancelledExitCode).kind === "cancelled"
? t("scriptTerminal.cancelled")
: exitCode === 0
? (completedSuccessfullyMessage ?? t("scriptTerminal.completedSuccessfully"))
: (completedWithErrorMessage?.(exitCode) ?? t("scriptTerminal.completedWithError", { code: exitCode })),
[completedSuccessfullyMessage, completedWithErrorMessage, t],
[completedSuccessfullyMessage, completedWithErrorMessage, cancelledExitCode, t],
)
const terminalContainerRef = useRef<HTMLDivElement>(null)
@@ -139,6 +144,11 @@ export function ScriptTerminalModal({
reconnectTimeoutRef.current = setTimeout(async () => {
if (wsRef.current?.readyState !== WebSocket.OPEN && termRef.current) {
if (wsRef.current) {
// Replacing a stale socket is not a script result. Its close event
// must not complete the operation before the new socket reports it.
wsRef.current.onclose = null
wsRef.current.onerror = null
wsRef.current.onmessage = null
wsRef.current.close()
}
@@ -176,6 +186,7 @@ const initMessage = {
}
ws.onmessage = (event) => {
if (completionReceivedRef.current) return
// Filter out pong responses from heartbeat
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
return
@@ -189,10 +200,10 @@ const initMessage = {
: null
if (exitMatch) {
const exitCode = Number(exitMatch[1])
termRef.current?.write(event.data)
if (scriptCompletion(exitCode, cancelledExitCode).kind !== "cancelled") termRef.current?.write(event.data)
completionReceivedRef.current = true
setIsComplete(true)
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
termRef.current?.writeln(scriptCompletionOutput(exitCode, getCompletionMessage(exitCode), cancelledExitCode))
onCompleteRef.current?.(exitCode)
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
return
@@ -223,7 +234,7 @@ const initMessage = {
const exitCode = Number(msg.exit_code ?? 1)
completionReceivedRef.current = true
setIsComplete(true)
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
termRef.current?.writeln(scriptCompletionOutput(exitCode, getCompletionMessage(exitCode), cancelledExitCode))
onCompleteRef.current?.(exitCode)
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
return
@@ -261,7 +272,7 @@ const initMessage = {
}
}
}, 1000)
}, [isOpen, isComplete, scriptPath, getCompletionMessage, t])
}, [isOpen, isComplete, scriptPath, getCompletionMessage, cancelledExitCode, t])
const sendKey = useCallback((key: string) => {
if (!termRef.current) return
@@ -392,6 +403,7 @@ const initMessage = {
}
ws.onmessage = (event) => {
if (completionReceivedRef.current) return
// Filter out pong responses from heartbeat - don't display in terminal
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
return
@@ -405,10 +417,10 @@ const initMessage = {
: null
if (exitMatch) {
const exitCode = Number(exitMatch[1])
term.write(event.data)
if (scriptCompletion(exitCode, cancelledExitCode).kind !== "cancelled") term.write(event.data)
completionReceivedRef.current = true
setIsComplete(true)
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
term.writeln(scriptCompletionOutput(exitCode, getCompletionMessage(exitCode), cancelledExitCode))
onCompleteRef.current?.(exitCode)
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
return
@@ -441,7 +453,7 @@ const initMessage = {
const exitCode = Number(msg.exit_code ?? 1)
completionReceivedRef.current = true
setIsComplete(true)
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
term.writeln(scriptCompletionOutput(exitCode, getCompletionMessage(exitCode), cancelledExitCode))
onCompleteRef.current?.(exitCode)
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
return
@@ -814,7 +826,7 @@ const initMessage = {
)}
{(isMobile || isTablet) && (
<div className="flex items-center justify-center gap-1.5 px-1 py-2 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex flex-wrap items-center justify-center gap-1.5 px-1 py-2 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button
onPointerDown={(e) => {
e.preventDefault()
+46 -17
View File
@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, useRef } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff, Globe2, Github } from "lucide-react"
import { Badge } from "./ui/badge"
@@ -20,6 +20,7 @@ import { SUPPORTED_LANGUAGES, useI18n } from "../lib/i18n/provider"
import { useIsPbs } from "../lib/use-platform"
import type { LanguageCode } from "../lib/i18n/languages"
import { NavTabOrderCard } from "./nav-tab-order-card"
import { optimizationSourceError, optimizationSourceName, type OptimizationSourceError, type OptimizationSourceResponse } from "../lib/optimization-source-errors"
// GitHub Dark color palette for bash syntax highlighting
const BASH_KEYWORDS = new Set([
@@ -355,15 +356,31 @@ export function Settings() {
open: boolean
loading: boolean
toolName: string
toolKey: string
version: string
availableVersion: string
functionName: string
source: string
script: string
error: string
error: OptimizationSourceError | null
deprecated: boolean
}>({ open: false, loading: false, toolName: '', version: '', availableVersion: '', functionName: '', source: '', script: '', error: '', deprecated: false })
}>({ open: false, loading: false, toolName: '', toolKey: '', version: '', availableVersion: '', functionName: '', source: '', script: '', error: null, deprecated: false })
const [codeCopied, setCodeCopied] = useState(false)
const codeRequest = useRef(0)
const closeCodeModal = () => {
codeRequest.current += 1
setCodeModal(prev => ({ ...prev, open: false }))
setCodeCopied(false)
}
useEffect(() => () => { codeRequest.current += 1 }, [])
useEffect(() => {
if (!codeModal.open) return
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") closeCodeModal()
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [codeModal.open])
// Health Monitor suppression settings
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
@@ -664,31 +681,39 @@ export function Settings() {
}
const viewToolSource = async (tool: ProxMenuxTool) => {
const request = ++codeRequest.current
setCodeCopied(false)
setCodeModal({
open: true,
loading: true,
toolName: tool.name,
toolKey: tool.key,
version: tool.version || '1.0',
availableVersion: tool.available_version || tool.version || '1.0',
functionName: '',
source: '',
script: '',
error: '',
error: null,
deprecated: !!tool.deprecated,
})
try {
const data = await fetchApi(`/api/proxmenux/tool-source/${tool.key}`)
if (data.success) {
setCodeModal(prev => ({ ...prev, loading: false, functionName: data.function, source: data.source, script: data.script, deprecated: !!data.deprecated }))
const data = await fetchApi<OptimizationSourceResponse | null>(`/api/proxmenux/tool-source/${tool.key}`)
if (request !== codeRequest.current) return
if (data?.success === true && typeof data.source === "string" && data.source.trim()) {
const source = data.source
setCodeModal(prev => ({ ...prev, loading: false, functionName: typeof data.function === "string" ? data.function : '', source, script: typeof data.script === "string" ? data.script : '', deprecated: !!data.deprecated }))
} else {
setCodeModal(prev => ({ ...prev, loading: false, error: data.error || 'Source code not available' }))
setCodeModal(prev => ({ ...prev, loading: false, error: { kind: "unavailable", detail: typeof data?.error === "string" ? data.error : undefined } }))
}
} catch {
setCodeModal(prev => ({ ...prev, loading: false, error: 'Failed to load source code' }))
} catch (error: any) {
if (request !== codeRequest.current) return
const detail = typeof error?.body?.error === "string" ? error.body.error : undefined
setCodeModal(prev => ({ ...prev, loading: false, error: { kind: "loadFailed", detail } }))
}
}
const copySourceCode = async () => {
const request = codeRequest.current
const text = codeModal.source
let ok = false
@@ -722,9 +747,9 @@ export function Settings() {
}
}
if (ok) {
if (ok && request === codeRequest.current) {
setCodeCopied(true)
setTimeout(() => setCodeCopied(false), 2000)
setTimeout(() => { if (request === codeRequest.current) setCodeCopied(false) }, 2000)
}
}
@@ -2154,10 +2179,13 @@ export function Settings() {
{/* Code Viewer Modal */}
{codeModal.open && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ margin: 0 }} onClick={closeCodeModal}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div
className="relative bg-card border border-border rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col"
role="dialog"
aria-modal="true"
aria-label={t("settings.optimizations.sourceDialogTitle", { name: optimizationSourceName(codeModal.toolKey, codeModal.toolName, t) })}
onClick={e => e.stopPropagation()}
>
{/* Header */}
@@ -2166,7 +2194,7 @@ export function Settings() {
<Code className={`h-5 w-5 flex-shrink-0 ${codeModal.deprecated ? 'text-amber-500' : 'text-orange-500'}`} />
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-semibold truncate">{codeModal.toolName}</h3>
<h3 className="text-sm font-semibold truncate">{optimizationSourceName(codeModal.toolKey, codeModal.toolName, t)}</h3>
{codeModal.deprecated && (
<span className="text-[9px] uppercase tracking-wider text-amber-500 bg-amber-500/10 border border-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">
{t("settings.optimizations.legacy")}
@@ -2202,7 +2230,8 @@ export function Settings() {
</button>
)}
<button
onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}
onClick={closeCodeModal}
aria-label={t("actions.close")}
className="p-1.5 rounded-md hover:bg-muted transition-colors"
>
<X className="h-4 w-4" />
@@ -2212,13 +2241,13 @@ export function Settings() {
{/* Body */}
<div className="flex-1 overflow-auto p-0">
{codeModal.loading ? (
<div className="flex items-center justify-center py-16">
<div className="flex items-center justify-center py-16" role="status" aria-label={t("settings.optimizations.sourceLoading")}>
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
</div>
) : codeModal.error ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Code className="h-10 w-10 mb-3 opacity-40" />
<p className="text-sm">{codeModal.error}</p>
<p className="text-sm" role="alert">{optimizationSourceError(codeModal.error, t)}</p>
</div>
) : (
<pre
+20 -6
View File
@@ -7,6 +7,8 @@ import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network, ChevronRight, Database } from "lucide-react"
import { DatastoreUsageRow, type PBSDatastore } from "./pbs-datastore-usage"
import { PBSProblemMessage } from "./pbs-problem-message"
import { pbsDiagnostic } from "../lib/pbs-messages"
import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { TemperatureDetailModal } from "./temperature-detail-modal"
@@ -187,14 +189,20 @@ const getUnitsSettings = (): "Bytes" | "Bits" => {
return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"
}
interface PBSOverviewStatus {
datastores: PBSDatastore[] | null
errors?: Record<string, string>
error_details?: Record<string, { code: string }>
}
export function SystemOverview({ product = "pve" }: { product?: "pve" | "pbs" } = {}) {
const t = useT()
const { language } = useI18n()
const isPbs = product === "pbs"
// On Proxmox Backup Server the datastores take the place of guests and PVE storage.
const { data: pbsStatus, error: pbsError } = useSWR<{ datastores: PBSDatastore[] | null }>(
const { data: pbsStatus, error: pbsError } = useSWR<PBSOverviewStatus>(
isPbs ? "/api/pbs/status?server=local" : null,
(url: string) => fetchApi<{ datastores: PBSDatastore[] | null }>(url),
(url: string) => fetchApi<PBSOverviewStatus>(url),
{ refreshInterval: 60000, revalidateOnFocus: false },
)
const datastores = pbsStatus?.datastores ?? null
@@ -545,13 +553,15 @@ export function SystemOverview({ product = "pve" }: { product?: "pve" | "pbs" }
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{!datastores && !pbsError ? (
{!pbsStatus && !pbsError ? (
<div className="space-y-2 animate-pulse">
<div className="h-10 bg-muted rounded w-20"></div>
<div className="h-5 bg-muted rounded w-32"></div>
</div>
) : !datastores ? (
<p className="text-sm text-amber-500">{t("pbs.loadFailed")}</p>
<div role="alert" className="text-sm text-amber-500">
<PBSProblemMessage problem={pbsError || pbsStatus?.error_details?.datastores} raw={pbsDiagnostic(pbsError) || pbsStatus?.errors?.datastores} />
</div>
) : datastores.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("pbs.noDatastores")}</p>
) : (
@@ -762,8 +772,12 @@ export function SystemOverview({ product = "pve" }: { product?: "pve" | "pbs" }
{isPbs ? (
<div className="space-y-3">
<div className="text-xs font-medium text-muted-foreground mb-2">{t("pbs.datastores")}</div>
{!datastores ? (
<div className="text-center py-4 text-muted-foreground text-sm">{pbsError ? t("pbs.loadFailed") : t("pbs.loading")}</div>
{!pbsStatus && !pbsError ? (
<div className="text-center py-4 text-muted-foreground text-sm">{t("pbs.loading")}</div>
) : !datastores ? (
<div role="alert" className="py-4 text-amber-500 text-sm">
<PBSProblemMessage problem={pbsError || pbsStatus?.error_details?.datastores} raw={pbsDiagnostic(pbsError) || pbsStatus?.errors?.datastores} />
</div>
) : datastores.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm">{t("pbs.noDatastores")}</div>
) : datastores.map(store => <DatastoreUsageRow key={store.name} store={store} />)}
+11 -4
View File
@@ -22,6 +22,7 @@ import useSWR from "swr"
import { MetricsView } from "./metrics-dialog"
import { LxcTerminalModal } from "./lxc-terminal-modal"
import { ScriptTerminalModal } from "./script-terminal-modal"
import { OCI_CANCELLED_EXIT_CODE, shouldRefreshOciAfterCompletion } from "@/lib/script-completion"
import { LxcAppPanel, ThemeAwareLogo, ociImageLabel } from "./lxc-app-panel"
import { GuestBackupList } from "./guest-backup-list"
import type { GuestBackup } from "../lib/guest-backup-archives"
@@ -977,7 +978,9 @@ export function VirtualMachines() {
// A container restored from a backup carries the mark of its installation
// and has no record on this host until it is recovered from the menu.
const [ociRestored, setOciRestored] = useState(false)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "modify" | "recover" } | null>(null)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "modify" | "recover"; recoverPrevious?: boolean } | null>(null)
const ociCompletionCodeRef = useRef<number | undefined>(undefined)
useEffect(() => { ociCompletionCodeRef.current = undefined }, [ociAction])
// Firewall log state — fetched only when the operator opens that tab
// so a CT/VM without firewall use doesn't pay the pvesh cost on every
@@ -5515,7 +5518,7 @@ export function VirtualMachines() {
<Button
size="sm"
className={updateBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "update" })}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "update", recoverPrevious: !!ociInstance.pending })}
>
{hasUpdate
? <ArrowUpCircle className="h-4 w-4 mr-1.5" />
@@ -5744,7 +5747,7 @@ export function VirtualMachines() {
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recover" })}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.recover")}
{t("vmLxc.ociUpdates.registerRestored")}
</Button>
</div>
)}
@@ -8014,6 +8017,7 @@ export function VirtualMachines() {
onClose={() => {
const vmid = ociAction.vmid
setOciAction(null)
if (!shouldRefreshOciAfterCompletion(ociCompletionCodeRef.current)) return
// The image, the digest and the record changed: read them again.
fetchApi(`/api/vms/${vmid}/apps/check`, { method: "POST" })
.catch(() => undefined)
@@ -8027,7 +8031,10 @@ export function VirtualMachines() {
}}
scriptPath="/usr/local/share/proxmenux/scripts/oci/manage_instance.sh"
scriptName="oci_manage_instance"
title={ociAction.action === "update" ? t("vmLxc.ociUpdates.terminalTitleUpdate")
cancelledExitCode={OCI_CANCELLED_EXIT_CODE}
onComplete={(exitCode) => { ociCompletionCodeRef.current = exitCode }}
title={ociAction.action === "update" ? t(ociAction.recoverPrevious
? "vmLxc.ociUpdates.terminalTitleRecoverPrevious" : "vmLxc.ociUpdates.terminalTitleUpdate")
: ociAction.action === "recover" ? t("vmLxc.ociUpdates.terminalTitleRecover")
: t("vmLxc.ociUpdates.terminalTitleModify")}
description={t("vmLxc.ociUpdates.terminalDescription")}
+141
View File
@@ -0,0 +1,141 @@
// Legacy HostBackup API messages are an English wire contract shared with CLI callers.
// Match complete, source-verified messages only; never derive a translation key from input.
// This presentation-only adapter keeps unknown tool diagnostics and parameter text intact.
type Translate = (key: string, params?: Record<string, string | number>) => string
const fixed = new Map<string, string>([
["path must be absolute", "backup.apiErrors.absolutePath"],
["this path is already configured", "backup.apiErrors.pathConfigured"],
["path is required", "backup.apiErrors.pathRequired"],
["path query parameter is required", "backup.apiErrors.pathQuery"],
["path not found", "backup.apiErrors.pathNotFound"],
["invalid job id", "backup.apiErrors.invalidJobId"],
["job not found", "backup.apiErrors.jobNotFound"],
["runner script not installed", "backup.apiErrors.runnerMissing"],
["no restore state to dismiss", "backup.apiErrors.noRestore"],
["invalid file name", "backup.apiErrors.invalidFile"],
["not found", "backup.apiErrors.notFound"],
["log path outside allowed directory", "backup.apiErrors.logOutside"],
["targets must be a list", "backup.apiErrors.targetsList"],
["empty expression", "backup.apiErrors.emptyExpression"],
["invalid id (use letters, digits, _ or -)", "backup.apiErrors.invalidId"],
["backend must be pbs, local or borg", "backup.apiErrors.backend"],
["backend must be pbs or borg", "backup.apiErrors.remoteBackend"],
["backend must be 'pbs' or 'borg'", "backup.apiErrors.remoteBackendQuoted"],
["profile_mode must be default or custom", "backup.apiErrors.profileMode"],
["borg cannot be attached (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttached"],
["borg is not a valid backend for attached jobs (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttachedLong"],
["pve_parent_job_id and pve_storage are required for attached", "backup.apiErrors.parentRequired"],
["paths must be a list of absolute paths", "backup.apiErrors.absolutePaths"],
["custom profile needs at least one path", "backup.apiErrors.customPath"],
["could not resolve default profile paths", "backup.apiErrors.defaultPaths"],
["retention must be an object", "backup.apiErrors.retentionObject"],
["pbs_repository is required", "backup.apiErrors.pbsRepository"],
["pbs_encrypt_mode must be 'none', 'new' or 'existing'", "backup.apiErrors.pbsEncryption"],
["failed to create PBS encryption keyfile", "backup.apiErrors.pbsKeyFailed"],
["failed to create PBS encryption keyfile (is proxmox-backup-client installed?)", "backup.apiErrors.pbsClientMissing"],
["borg_repo is required", "backup.apiErrors.borgRepository"],
["borg_encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.borgEncryption"],
["encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.encryption"],
["borg_passphrase is required when encryption is enabled (save it on the destination or pass one in the request)", "backup.apiErrors.borgPassphrase"],
["passphrase is required when encryption is enabled", "backup.apiErrors.passphrase"],
["password is required", "backup.apiErrors.password"],
["confirm_device must match device exactly (typo / safety check)", "backup.apiErrors.deviceConfirmation"],
["invalid device path", "backup.apiErrors.invalidDevice"],
["absolute mountpoint required", "backup.apiErrors.absoluteMountpoint"],
["archive layout not recognized — no rootfs/ found", "backup.apiErrors.archiveLayout"],
["archive not found", "backup.apiErrors.archiveMissing"],
["archive path is not a regular file", "backup.apiErrors.archiveNotFile"],
["invalid name", "backup.apiErrors.invalidName"],
["invalid staging_path", "backup.apiErrors.invalidStaging"],
["job name cannot be changed (delete + recreate to rename)", "backup.apiErrors.jobRename"],
["key_path must be absolute", "backup.apiErrors.absoluteKey"],
["mode must be \"local\" or \"ssh\"", "backup.apiErrors.mode"],
["name is required (letters, digits, _ or -)", "backup.apiErrors.nameRequired"],
["no log available for this job yet", "backup.apiErrors.noLog"],
["no manual PBS configs file", "backup.apiErrors.noPbsConfig"],
["output file missing", "backup.apiErrors.outputMissing"],
["parser output was not valid JSON", "backup.apiErrors.parserJson"],
["run_restore output was not valid JSON", "backup.apiErrors.restoreJson"],
["repo is required", "backup.apiErrors.repoRequired"],
["repo_name and snapshot are required", "backup.apiErrors.repoSnapshot"],
["repo_name and snapshot are required for borg", "backup.apiErrors.borgRepoSnapshot"],
["repo_name and snapshot are required for pbs", "backup.apiErrors.pbsRepoSnapshot"],
["repo_name is required", "backup.apiErrors.repoName"],
["restore tooling not installed on this host", "backup.apiErrors.restoreMissing"],
["server and datastore are required", "backup.apiErrors.serverDatastore"],
["snapshot is required", "backup.apiErrors.snapshotRequired"],
["source must be pbs, borg, or local", "backup.apiErrors.source"],
["ssh_port must be an integer between 1 and 65535", "backup.apiErrors.sshPort"],
["ssh_user, ssh_host and ssh_remote_path are required for ssh mode", "backup.apiErrors.sshRequired"],
["target has no repository path", "backup.apiErrors.repositoryPath"],
["task not found or expired", "backup.apiErrors.taskExpired"],
["invalid expression", "backup.apiErrors.invalidExpression"],
["no report emitted", "backup.apiErrors.noReport"],
["parse_manifest exited non-zero", "backup.apiErrors.parseManifestFailed"],
])
const parameterized: ReadonlyArray<readonly [RegExp, string]> = [
[new RegExp("^job already exists: ([\\s\\S]+)$"), "backup.apiErrors.jobExists"],
[new RegExp("^path does not exist on this host: ([\\s\\S]+)$"), "backup.apiErrors.pathMissing"],
[new RegExp("^device does not exist: ([\\s\\S]+)$"), "backup.apiErrors.deviceMissing"],
[new RegExp("^not a mountpoint: ([\\s\\S]+)$"), "backup.apiErrors.mountpoint"],
[new RegExp("^local archive not found: ([\\s\\S]+)$"), "backup.apiErrors.localArchiveMissing"],
[new RegExp("^Borg destination '([\\s\\S]+)' not found$"), "backup.apiErrors.borgDestination"],
[new RegExp("^PBS destination '([\\s\\S]+)' not found$"), "backup.apiErrors.pbsDestination"],
[new RegExp("^no password available for PBS repo '([\\s\\S]+)'$"), "backup.apiErrors.pbsPassword"],
[new RegExp("^Borg repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.borgRepoMissing"],
[new RegExp("^PBS repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.pbsRepoMissing"],
[new RegExp("^Borg target not found: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetMissing"],
[new RegExp("^PBS repo not found: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetMissing"],
[new RegExp("^PVE vzdump job not found: ([\\s\\S]+)$"), "backup.apiErrors.pveJobMissing"],
[new RegExp("^cannot persist state: ([\\s\\S]+)$"), "backup.apiErrors.statePersist"],
[new RegExp("^cannot prepare ssh dir: ([\\s\\S]+)$"), "backup.apiErrors.sshDirectory"],
[new RegExp("^cannot read log: ([\\s\\S]+)$"), "backup.apiErrors.logRead"],
[new RegExp("^could not update \\.env: ([\\s\\S]+)$"), "backup.apiErrors.envUpdate"],
[new RegExp("^failed to persist PBS config: ([\\s\\S]+)$"), "backup.apiErrors.pbsConfigWrite"],
[new RegExp("^failed to persist: ([\\s\\S]+)$"), "backup.apiErrors.persist"],
[new RegExp("^failed to remove archive: ([\\s\\S]+)$"), "backup.apiErrors.archiveRemove"],
[new RegExp("^failed to start: ([\\s\\S]+)$"), "backup.apiErrors.start"],
[new RegExp("^failed to update borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsUpdate"],
[new RegExp("^failed to update pbs-manual-configs\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetsUpdate"],
[new RegExp("^failed to update systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsUpdate"],
[new RegExp("^failed to write borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsWrite"],
[new RegExp("^failed to write job files: ([\\s\\S]+)$"), "backup.apiErrors.jobWrite"],
[new RegExp("^failed to write local-target\\.conf: ([\\s\\S]+)$"), "backup.apiErrors.localTargetWrite"],
[new RegExp("^failed to write systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsWrite"],
[new RegExp("^format failed: ([\\s\\S]+)$"), "backup.apiErrors.format"],
[new RegExp("^invalid on_calendar: ([\\s\\S]+)$"), "backup.apiErrors.calendar"],
[new RegExp("^mount failed: ([\\s\\S]+)$"), "backup.apiErrors.mount"],
[new RegExp("^parser invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.parserInvocation"],
[new RegExp("^preflight invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.preflightInvocation"],
[new RegExp("^public key not found after generation: ([\\s\\S]+)$"), "backup.apiErrors.publicKeyMissing"],
[new RegExp("^retention\\.([\\s\\S]+) must be an integer$"), "backup.apiErrors.retentionInteger"],
[new RegExp("^retention\\.([\\s\\S]+) must be non-negative$"), "backup.apiErrors.retentionNegative"],
[new RegExp("^ssh-keygen failed: ([\\s\\S]+)$"), "backup.apiErrors.keygen"],
[new RegExp("^systemctl failed: ([\\s\\S]+)$"), "backup.apiErrors.systemctl"],
[new RegExp("^umount failed: ([\\s\\S]+)$"), "backup.apiErrors.unmount"],
[new RegExp("^task is in state \"([\\s\\S]+)\", not completed$"), "backup.apiErrors.taskIncomplete"],
[new RegExp("^unknown kind: ([\\s\\S]+)$"), "backup.apiErrors.unknownKind"],
[new RegExp("^unknown mode \"([\\s\\S]+)\"$"), "backup.apiErrors.unknownMode"],
[new RegExp("^([\\s\\S]+) is the built-in default and cannot be removed$"), "backup.apiErrors.defaultRemove"],
[new RegExp("^([\\s\\S]+) is the built-in default; no need to add it$"), "backup.apiErrors.defaultAdd"],
[new RegExp("^pbs_encrypt_mode='existing' but no keyfile is installed at ([\\s\\S]+) — import one first with POST /api/host-backups/pbs-encryption/import$"), "backup.apiErrors.keyImport"],
[new RegExp("^PVE job storage mismatch \\(job uses ([\\s\\S]+), payload says ([\\s\\S]+)\\)$"), "backup.apiErrors.storageMismatch"],
]
export function hostBackupErrorText(problem: unknown, t: Translate): string {
const message = typeof problem === "string" ? problem
: problem && typeof problem === "object" && "message" in problem && typeof problem.message === "string"
? problem.message : ""
if (!message) return t("backup.apiErrors.requestFailed")
const key = fixed.get(message)
if (key) return t(key)
// Keep unusually large tool output verbatim, without running pattern matching over it.
if (message.length > 16384) return message
for (const [pattern, patternKey] of parameterized) {
const match = pattern.exec(message)
if (match) return t(patternKey, { value: match[1], other: match[2] ?? "" })
}
return message
}
+130
View File
@@ -0,0 +1,130 @@
type Translate = (key: string, params?: Record<string, string | number>) => string
// Exact lxc_apps validation/persistence messages; never change their API contract.
// Anchored patterns leave unknown diagnostics intact, including paths and commands.
const RULES: [RegExp, string, string[]][] = [
[/^an OCI operation on this container is running$/, "ociBusy", []],
[/^([\s\S]+) is not installed via dpkg$/, "dpkgMissing", ["package"]],
[/^dpkg\-query failed$/, "dpkgFailed", []],
[/^no version matched in dpkg output$/, "dpkgNoVersion", []],
[/^apk info failed$/, "apkFailed", []],
[/^no version matched in apk output$/, "apkNoVersion", []],
[/^could not read ([\s\S]+)$/, "fileRead", ["path"]],
[/^file_regex did not match ([\s\S]+)$/, "fileNoMatch", ["path"]],
[/^binary invocation failed$/, "binaryFailed", []],
[/^no version matched in binary output$/, "binaryNoVersion", []],
[/^python \-c importlib\.metadata failed$/, "pythonFailed", []],
[/^no version matched in Python distribution output$/, "pythonNoVersion", []],
[/^could not resolve the container's installed image$/, "dockerImageResolve", []],
[/^docker inspect failed$/, "dockerInspectFailed", []],
[/^container has no ([\s\S]+) label$/, "dockerLabelMissing", ["label"]],
[/^docker label reports ([\s\S]+) \(mutable tag, not a version\)$/, "dockerLabelMutable", ["tag"]],
[/^no version matched in docker label output$/, "dockerLabelNoVersion", []],
[/^docker exec failed$/, "dockerExecFailed", []],
[/^no version matched in docker exec output$/, "dockerExecNoVersion", []],
[/^command_argv is empty$/, "commandEmpty", []],
[/^command failed$/, "commandFailed", []],
[/^no version matched in command output$/, "commandNoVersion", []],
[/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
[/^unknown upstream_type: ([\s\S]+)$/, "unknownUpstream", ["type"]],
[/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+): not found$/, "githubNotFound", ["repo"]],
[/^github rejected the request \(403\)$/, "githubRejected", []],
[/^github error (\d+)$/, "githubError", ["code"]],
[/^no tag \/ release name in response$/, "noRelease", []],
[/^tag_regex did not match '([\s\S]+)'$/, "tagNoMatch", ["tag"]],
[/^http error (\d+)$/, "httpError", ["code"]],
[/^invalid JSON response: ([\s\S]+)$/, "invalidJson", ["detail"]],
[/^json_path '([\s\S]+)' did not resolve$/, "jsonPathUnresolved", ["path"]],
[/^empty value at json_path$/, "jsonEmpty", []],
[/^no tags matched \(docker_hub\)$/, "dockerNoTags", []],
[/^tag_regex matched only moving tags; use Docker image digest tracking$/, "dockerMovingOnly", []],
[/^docker hub error (\d+)$/, "dockerHubError", ["code"]],
[/^invalid docker hub JSON: ([\s\S]+)$/, "dockerHubJson", ["detail"]],
[/^no tags returned by docker hub$/, "dockerHubEmpty", []],
[/^payload must be a JSON object$/, "payload", []],
[/^name is required and must be 1\-64 chars of letters\/digits\/spaces\/\._\+\-\(\)\/$/, "name", []],
[/^installed_via must be one of: ([\s\S]+) or empty$/, "method", ["values"]],
[/^package is required \(letters\/digits\/\._\+@:\/ up to 127 chars\)$/, "package", []],
[/^file_path is required and must be an absolute path$/, "filePath", []],
[/^file_regex is required$/, "fileRegex", []],
[/^file_regex is not a valid regex: ([\s\S]+)$/, "fileRegexInvalid", ["detail"]],
[/^binary_path is required and must be an absolute path$/, "binaryPath", []],
[/^python_path is required and must be an absolute path$/, "pythonPath", []],
[/^distribution is required \(PEP 503 name\)$/, "distribution", []],
[/^container_name is required \(docker naming rules\)$/, "container", []],
[/^label is required \(OCI label naming rules\)$/, "label", []],
[/^binary_path is required \(absolute path or bare command\)$/, "dockerBinary", []],
[/^installed_version is required for the manual method$/, "manualVersion", []],
[/^installed_version exceeds (\d+) chars$/, "versionLength", ["max"]],
[/^installed_version contains a forbidden control character$/, "versionControl", []],
[/^installed_regex is not a valid regex: ([\s\S]+)$/, "installedRegexInvalid", ["detail"]],
[/^upstream_type must be one of: ([\s\S]+)$/, "upstreamType", ["values"]],
[/^tag_regex is not a valid regex: ([\s\S]+)$/, "tagRegexInvalid", ["detail"]],
[/^repo is required for upstream_type=github$/, "repoRequired", []],
[/^repo must be 'owner\/name'$/, "repoFormat", []],
[/^github_source must be one of: ([\s\S]+)$/, "githubSource", ["values"]],
[/^upstream_url is required for upstream_type=http_json$/, "upstreamUrlRequired", []],
[/^upstream_url must be an http\(s\) URL$/, "upstreamUrl", []],
[/^upstream_url exceeds (\d+) chars$/, "upstreamUrlLength", ["max"]],
[/^upstream_json_path is required for upstream_type=http_json$/, "jsonPathRequired", []],
[/^upstream_json_path exceeds (\d+) chars$/, "jsonPathLength", ["max"]],
[/^upstream_json_path uses forbidden characters$/, "jsonPathCharacters", []],
[/^docker_image is required for upstream_type=docker_hub$/, "dockerImageRequired", []],
[/^docker_image exceeds (\d+) chars$/, "dockerImageLength", ["max"]],
[/^docker_image must match Docker Hub naming \(owner\/name or name\)$/, "dockerImageFormat", []],
[/^health_path must be a valid URL path \(max 255 chars\)$/, "healthPath", []],
[/^logo_url must be an http\(s\) URL or this VM's cached local icon$/, "logoUrl", []],
[/^invalid discovery_id$/, "discoveryId", []],
[/^helper_slug must be a lowercase slug \(letters\/digits\/\._\-\)$/, "helperSlug", []],
[/^update_via only accepts 'docker'$/, "updateVia", []],
[/^update_via=docker requires a docker_label or docker_exec detector$/, "dockerDetector", []],
[/^update_via=docker cannot be combined with ([\s\S]+)$/, "dockerConflict", ["field"]],
[/^update_command must be a string$/, "updateCommandType", []],
[/^update_command exceeds (\d+) chars$/, "updateCommandLength", ["max"]],
[/^update_command contains a null byte$/, "updateCommandNull", []],
[/^update_method must be none, helper or custom$/, "updateMethod", []],
[/^update_command is required for update_method=custom$/, "updateCommandRequired", []],
[/^update_command is only allowed for update_method=custom$/, "updateCommandOnlyCustom", []],
[/^a supported helper_slug is required for update_method=helper$/, "helperRequired", []],
[/^Docker\-delegated apps update through their image, not a separate app updater$/, "dockerUpdater", []],
[/^command_argv is required \(non\-empty list\)$/, "commandRequired", []],
[/^command_argv must be a non\-empty list of strings$/, "commandList", []],
[/^command_argv accepts at most (\d+) entries$/, "commandCount", ["max"]],
[/^command_argv\[(\d+)\] must be a non\-empty string$/, "commandEntry", ["index"]],
[/^command_argv\[(\d+)\] exceeds (\d+) chars$/, "commandEntryLength", ["index", "max"]],
[/^command_argv\[(\d+)\] contains a forbidden control character$/, "commandEntryControl", ["index"]],
[/^binary_args must be a list of strings$/, "binaryArgsList", []],
[/^binary_args accepts at most (\d+) entries$/, "binaryArgsCount", ["max"]],
[/^binary_args\[(\d+)\] must be a non\-empty string$/, "binaryArgEntry", ["index"]],
[/^binary_args\[(\d+)\] exceeds (\d+) chars$/, "binaryArgLength", ["index", "max"]],
[/^binary_args\[(\d+)\] contains a forbidden control character$/, "binaryArgControl", ["index"]],
[/^ports must be a list of \{port, description, scheme\}$/, "portsList", []],
[/^ports\[(\d+)\] must be an object$/, "portObject", ["index"]],
[/^ports\[(\d+)\]\.port is required$/, "portRequired", ["index"]],
[/^ports\[(\d+)\]\.port must be an integer$/, "portInteger", ["index"]],
[/^ports\[(\d+)\]\.port must be 1\-65535$/, "portRange", ["index"]],
[/^port (\d+) appears more than once for this app$/, "portDuplicate", ["port"]],
[/^ports\[(\d+)\]\.description has invalid characters$/, "portDescription", ["index"]],
[/^ports\[(\d+)\]\.scheme must be 'http' or 'https'$/, "portScheme", ["index"]],
[/^ports\[(\d+)\]\.web_path must be a valid URL path \(max 255 chars\)$/, "portPath", ["index"]],
[/^ports\[(\d+)\]\.logo_url must be an http\(s\) URL \(max 512 chars\)$/, "portLogo", ["index"]],
[/^ports\[(\d+)\]\.category has invalid characters or is too long$/, "portCategory", ["index"]],
[/^ports\[(\d+)\]\.custom_url must be an http\(s\) URL \(max 512 chars\)$/, "portCustomUrl", ["index"]],
[/^tag_regex exceeds 512 chars$/, "tagRegexLength", []],
[/^could not persist sidecar \(permission\?\)$/, "persistPermission", []],
[/^could not persist sidecar$/, "persist", []],
[/^no apps registered for this vmid$/, "noApps", []],
[/^app_id '([\s\S]+)' not found$/, "appMissing", ["appId"]],
[/^app not found$/, "appNotFound", []],
]
export function localizeAppEditorError(message: string | null | undefined, t: Translate): string {
if (!message) return ""
for (const [pattern, key, names] of RULES) {
const match = pattern.exec(message)
if (!match) continue
const params = Object.fromEntries(names.map((name, index) => [name, match[index + 1]]))
return t(`vmLxc.appEditor.errors.${key}`, params)
}
return message
}
+29
View File
@@ -0,0 +1,29 @@
type Translate = (key: string, params?: Record<string, string | number>) => string
const ERROR_KEYS: Record<string, string> = {
monitor_link_address_required: "addressRequired",
monitor_link_invalid_address: "invalidAddress",
monitor_link_address_credentials: "addressCredentials",
monitor_link_invalid_port: "invalidPort",
monitor_link_invalid_request: "invalidRequest",
monitor_link_duplicate: "duplicate",
monitor_link_not_found: "notFound",
monitor_link_detected_remove: "detectedRemove",
}
// fetchApi preserves the structured response on Error.body. Never expose raw
// server/network messages or treat arbitrary error codes as translation keys.
export function monitorLinkError(error: unknown, t: Translate): string {
const fallback = () => t("monitorLinks.saveFailed")
if (!error || typeof error !== "object" || !("body" in error)) return fallback()
const body = error.body
if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return fallback()
if (body.code === "monitor_link_limit") {
const params = "params" in body ? body.params : null
const count = params && typeof params === "object" && "count" in params ? params.count : null
return typeof count === "number" && Number.isSafeInteger(count) && count > 0
? t("monitorLinks.errors.limit", { count }) : fallback()
}
return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code)
? t(`monitorLinks.errors.${ERROR_KEYS[body.code]}`) : fallback()
}
@@ -0,0 +1,39 @@
type Translate = (key: string, params?: Record<string, string | number>) => string
const SOURCE_TOOLS = new Set([
"subscription_banner", "time_sync", "apt_languages", "journald", "logrotate",
"system_limits", "memory_settings", "zram_swap", "kernel_panic", "apt_ipv4",
"kexec", "rpc", "motd", "system_utils", "network_optimization", "bashrc_custom",
"figurine", "fastfetch", "log2ram", "zfs_autotrim", "amd_fixes", "persistent_network",
"vfio_iommu", "lvm_repair", "repo_cleanup", "proxmox_repos", "entropy",
])
export function optimizationSourceName(tool: string, fallback: string, t: Translate): string {
return SOURCE_TOOLS.has(tool) ? t(`settings.optimizations.sourceToolNames.${tool}`) : fallback
}
export interface OptimizationSourceError {
kind: "unavailable" | "loadFailed"
detail?: string
}
export interface OptimizationSourceResponse {
success?: unknown
source?: unknown
function?: unknown
script?: unknown
deprecated?: unknown
error?: unknown
}
// Keep the blueprint's English API contract intact; translate only its display.
export function optimizationSourceError(error: OptimizationSourceError, t: Translate): string {
const detail = error.detail || ""
const key = "settings.optimizations."
if (detail === "Function not found in available scripts") return t(key + "sourceFunctionMissing")
if (detail.startsWith("Unknown tool: ")) return t(key + "sourceUnknownTool", { tool: detail.slice(14) })
if (detail.startsWith("No function mapping for ")) return t(key + "sourceNoMapping", { tool: detail.slice(24) })
if (detail === "Source code not available") return t(key + "sourceUnavailable")
const message = t(key + (error.kind === "unavailable" ? "sourceUnavailable" : "sourceLoadFailed"))
return detail ? `${message} ${detail}` : message
}
+85
View File
@@ -0,0 +1,85 @@
type Translate = (key: string, params?: Record<string, string | number>) => string
const ERROR_KEYS: Record<string, string> = {
pbs_config_read: "pbs.errors.configRead", pbs_invalid_config: "pbs.errors.invalidConfig",
pbs_invalid_field: "pbs.errors.invalidField", pbs_invalid_url: "pbs.errors.invalidUrl",
pbs_name_required: "pbs.errors.nameRequired", pbs_token_id_format: "pbs.errors.tokenIdFormat",
pbs_token_secret: "pbs.errors.tokenSecret", pbs_invalid_token_id: "pbs.errors.invalidTokenId",
pbs_invalid_fingerprint: "pbs.errors.invalidFingerprint", pbs_fingerprint_mismatch: "pbs.errors.fingerprintMismatch",
pbs_token_rejected: "pbs.errors.tokenRejected", pbs_invalid_response: "pbs.errors.invalidResponse",
pbs_invalid_json: "pbs.errors.invalidJson", pbs_connect_failed: "pbs.errors.connectFailed",
pbs_invalid_id: "pbs.errors.invalidId", pbs_invalid_datastores: "pbs.errors.invalidDatastores",
pbs_username: "pbs.errors.username", pbs_password: "pbs.errors.password",
pbs_login_rejected: "pbs.errors.loginRejected", pbs_invalid_login: "pbs.errors.invalidLogin",
pbs_token_create_denied: "pbs.errors.tokenCreateDenied", pbs_invalid_token: "pbs.errors.invalidToken",
pbs_audit_role_denied: "pbs.errors.auditRoleDenied", pbs_not_found: "pbs.errors.notFound",
pbs_read_failed: "pbs.errors.readFailed", pbs_invalid_list: "pbs.errors.invalidList",
pbs_local_unavailable: "pbs.errors.localUnavailable", pbs_auth_required: "pbs.errors.authRequired",
pbs_save_failed: "pbs.saveFailed", pbs_remove_failed: "pbs.removeFailed",
pbs_saved_refresh_failed: "pbs.savedRefreshFailed", pbs_removed_refresh_failed: "pbs.removedRefreshFailed",
pbs_datastore_offline: "pbs.errors.datastoreOffline", pbs_datastore_unavailable: "pbs.errors.datastoreUnavailable",
}
// Accept structured codes, never arbitrary error text as a translation key.
function pbsErrorTranslation(error: unknown): { key: string; params?: { count: number } } | null {
const body = error && typeof error === "object" && "body" in error ? error.body : error
if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return null
if (body.code === "pbs_connection_limit") {
const params = "params" in body ? body.params : null
const count = params && typeof params === "object" && "count" in params ? params.count : null
return typeof count === "number" && Number.isSafeInteger(count) && count > 0
? { key: "pbs.errors.connectionLimit", params: { count } } : null
}
return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? { key: ERROR_KEYS[body.code] } : null
}
export function pbsIsKnownError(error: unknown): boolean {
return pbsErrorTranslation(error) !== null
}
export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string {
const translated = pbsErrorTranslation(error)
return translated ? t(translated.key, translated.params) : t(fallback)
}
export function pbsDiagnostic(error: unknown): string | undefined {
if (!error || typeof error !== "object") return undefined
const body = "body" in error ? error.body : null
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") return body.error
return "message" in error && typeof error.message === "string" ? error.message : undefined
}
const TASK_TYPES: Record<string, string> = {
backup: "backup", reader: "restore", verify: "verify", verificationjob: "verify",
verify_group: "verifyGroup", verify_snapshot: "verifySnapshot", prune: "prune", prunejob: "prune",
garbage_collection: "garbageCollection", sync: "sync", syncjob: "sync", aptupdate: "updateCheck",
termproxy: "terminal", dircreate: "createDatastore", "create-datastore": "createDatastore",
}
export function pbsTaskType(type: string | undefined, t: Translate): string {
return type && Object.prototype.hasOwnProperty.call(TASK_TYPES, type)
? t(`pbs.taskTypes.${TASK_TYPES[type]}`) : t("pbs.taskTypes.other", { type: type || "—" })
}
export function pbsResultText(status: string | undefined, t: Translate): string {
if (!status || status === "unknown") return t("pbs.states.unknown")
if (status.toLowerCase() === "ok") return t("pbs.states.ok")
const warnings = /^WARNINGS:\s*(\d+)\s*$/i.exec(status)
if (warnings && Number.isSafeInteger(Number(warnings[1]))) return t("pbs.states.warnings", { count: Number(warnings[1]) })
return /^warn/i.test(status) ? t("pbs.states.warning") : t("pbs.states.error")
}
export function pbsServiceState(state: string, t: Translate): string {
const known = ["active", "inactive", "failed", "activating", "deactivating", "unknown"]
return t(`pbs.serviceStates.${known.includes(state) ? state : "unknown"}`)
}
export function formatPBSDuration(seconds: number, t: Translate): string {
if (!Number.isFinite(seconds) || seconds < 0) return "—"
const unit = (key: string, count: number) => t(`pbs.durationUnits.${key}`, { count })
if (seconds < 60) return unit("seconds", Math.round(seconds))
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${unit("minutes", minutes)} ${unit("seconds", Math.round(seconds % 60))}`
const hours = Math.floor(minutes / 60)
return hours < 24 ? `${unit("hours", hours)} ${unit("minutes", minutes % 60)}`
: `${unit("days", Math.floor(hours / 24))} ${unit("hours", hours % 24)}`
}
+22
View File
@@ -0,0 +1,22 @@
// Native OCI `manage` reserves this code for explicit user cancellation.
export const OCI_CANCELLED_EXIT_CODE = 6
export function scriptCompletion(exitCode: number, cancelledExitCode?: number) {
if (exitCode === 0) return { kind: "success", color: "32" } as const
if (cancelledExitCode !== undefined && exitCode === cancelledExitCode) {
return { kind: "cancelled", color: "36" } as const
}
return { kind: "error", color: "31" } as const
}
export function shouldRefreshOciAfterCompletion(exitCode?: number) {
return exitCode !== OCI_CANCELLED_EXIT_CODE
}
export function scriptCompletionOutput(exitCode: number, message: string, cancelledExitCode?: number) {
const completion = scriptCompletion(exitCode, cancelledExitCode)
// Queue RIS with the result so earlier ncurses writes finish before reset.
// A synchronous terminal.reset() could be overwritten by that pending output.
const reset = completion.kind === "cancelled" ? "\x1bc" : ""
return `${reset}\x1b[${completion.color}m${message}\x1b[0m`
}
+375 -8
View File
@@ -1231,7 +1231,9 @@
"update": "Aktualisieren",
"modify": "Ändern",
"recover": "Wiederherstellen",
"registerRestored": "Erneut registrieren",
"terminalTitleUpdate": "OCI-Anwendung aktualisieren",
"terminalTitleRecoverPrevious": "Vorherige OCI-Installation wiederherstellen",
"terminalTitleModify": "OCI-Anwendung ändern",
"terminalTitleRecover": "Wiederhergestellte OCI-Anwendung registrieren",
"terminalDescription": "Es läuft derselbe Vorgang wie im ProxMenux-Menü: OCI manager Apps → Installierte OCI-Anwendungen verwalten.",
@@ -1760,7 +1762,128 @@
"dockerEvidenceImage": "Image",
"dockerEvidencePort": "Veröffentlichter TCP-Port",
"dockerEvidenceProject": "Compose-Projekt",
"dockerEndpointReview": "Container mit veröffentlichten TCP-Ports. Bestätigen Sie Namen und Webadresse vor dem Speichern. Die Registrierung richtet keine Updates ein."
"dockerEndpointReview": "Container mit veröffentlichten TCP-Ports. Bestätigen Sie Namen und Webadresse vor dem Speichern. Die Registrierung richtet keine Updates ein.",
"confirmRemove": "Den Eintrag dieser Anwendung aus der Registerkarte App entfernen? Die Anwendung wird dadurch nicht deinstalliert.",
"packageExample": "z. B. {package}",
"packageHelpBefore": "Automatisch aus dem Namen ausgefüllt. Prüfen Sie dies mit",
"packageHelpAfter": "innerhalb des CT.",
"binaryHelpBefore": "Absoluter Pfad. Ermitteln Sie ihn mit",
"binaryHelpOr": "oder",
"errors": {
"ociBusy": "ein OCI-Vorgang an diesem Container läuft",
"dpkgMissing": "{package} ist nicht über dpkg installiert",
"dpkgFailed": "dpkg-query ist fehlgeschlagen",
"dpkgNoVersion": "keine passende Version in der dpkg-Ausgabe",
"apkFailed": "apk info ist fehlgeschlagen",
"apkNoVersion": "keine passende Version in der apk-Ausgabe",
"fileRead": "{path} konnte nicht gelesen werden",
"fileNoMatch": "file_regex hat in {path} keine Übereinstimmung gefunden",
"binaryFailed": "Aufruf der Binärdatei fehlgeschlagen",
"binaryNoVersion": "keine passende Version in der Ausgabe der Binärdatei",
"pythonFailed": "python -c importlib.metadata ist fehlgeschlagen",
"pythonNoVersion": "keine passende Version in der Ausgabe der Python-Distribution",
"dockerImageResolve": "das installierte Image des Containers konnte nicht ermittelt werden",
"dockerInspectFailed": "docker inspect ist fehlgeschlagen",
"dockerLabelMissing": "der Container hat kein Label {label}",
"dockerLabelMutable": "das Docker-Label meldet {tag} (veränderlicher Tag, keine Version)",
"dockerLabelNoVersion": "keine passende Version in der Ausgabe des Docker-Labels",
"dockerExecFailed": "docker exec ist fehlgeschlagen",
"dockerExecNoVersion": "keine passende Version in der Ausgabe von docker exec",
"commandEmpty": "command_argv ist leer",
"commandFailed": "Befehl fehlgeschlagen",
"commandNoVersion": "keine passende Version in der Befehlsausgabe",
"unsupportedMethod": "nicht unterstützte Methode: {method}",
"unknownUpstream": "unbekannter upstream_type: {type}",
"githubNotFound": "{repo}: nicht gefunden",
"githubRejected": "GitHub hat die Anfrage abgelehnt (403)",
"githubError": "GitHub-Fehler {code}",
"noRelease": "kein Tag / Release-Name in der Antwort",
"tagNoMatch": "tag_regex passt nicht auf '{tag}'",
"httpError": "HTTP-Fehler {code}",
"invalidJson": "ungültige JSON-Antwort: {detail}",
"jsonPathUnresolved": "json_path '{path}' konnte nicht aufgelöst werden",
"jsonEmpty": "leerer Wert bei json_path",
"dockerNoTags": "keine passenden Tags (docker_hub)",
"dockerMovingOnly": "tag_regex passt nur auf bewegliche Tags; verwenden Sie die Digest-Verfolgung der Docker-Images",
"dockerHubError": "Docker-Hub-Fehler {code}",
"dockerHubJson": "ungültiges JSON von Docker Hub: {detail}",
"dockerHubEmpty": "Docker Hub hat keine Tags zurückgegeben",
"payload": "die Nutzdaten müssen ein JSON-Objekt sein",
"name": "name ist erforderlich und muss 1-64 Zeichen aus Buchstaben/Ziffern/Leerzeichen/._+-()/ enthalten",
"method": "installed_via muss einer der folgenden Werte sein: {values} oder leer",
"package": "package ist erforderlich (Buchstaben/Ziffern/._+@:/ bis zu 127 Zeichen)",
"filePath": "file_path ist erforderlich und muss ein absoluter Pfad sein",
"fileRegex": "file_regex ist erforderlich",
"fileRegexInvalid": "file_regex ist kein gültiger regulärer Ausdruck: {detail}",
"binaryPath": "binary_path ist erforderlich und muss ein absoluter Pfad sein",
"pythonPath": "python_path ist erforderlich und muss ein absoluter Pfad sein",
"distribution": "distribution ist erforderlich (Name nach PEP 503)",
"container": "container_name ist erforderlich (Docker-Namensregeln)",
"label": "label ist erforderlich (OCI-Namensregeln für Labels)",
"dockerBinary": "binary_path ist erforderlich (absoluter Pfad oder bloßer Befehl)",
"manualVersion": "installed_version ist für die manuelle Methode erforderlich",
"versionLength": "installed_version überschreitet {max} Zeichen",
"versionControl": "installed_version enthält ein unzulässiges Steuerzeichen",
"installedRegexInvalid": "installed_regex ist kein gültiger regulärer Ausdruck: {detail}",
"upstreamType": "upstream_type muss einer der folgenden Werte sein: {values}",
"tagRegexInvalid": "tag_regex ist kein gültiger regulärer Ausdruck: {detail}",
"repoRequired": "repo ist für upstream_type=github erforderlich",
"repoFormat": "repo muss 'owner/name' sein",
"githubSource": "github_source muss einer der folgenden Werte sein: {values}",
"upstreamUrlRequired": "upstream_url ist für upstream_type=http_json erforderlich",
"upstreamUrl": "upstream_url muss eine http(s)-URL sein",
"upstreamUrlLength": "upstream_url überschreitet {max} Zeichen",
"jsonPathRequired": "upstream_json_path ist für upstream_type=http_json erforderlich",
"jsonPathLength": "upstream_json_path überschreitet {max} Zeichen",
"jsonPathCharacters": "upstream_json_path verwendet unzulässige Zeichen",
"dockerImageRequired": "docker_image ist für upstream_type=docker_hub erforderlich",
"dockerImageLength": "docker_image überschreitet {max} Zeichen",
"dockerImageFormat": "docker_image muss der Docker-Hub-Benennung entsprechen (owner/name oder name)",
"healthPath": "health_path muss ein gültiger URL-Pfad sein (max. 255 Zeichen)",
"logoUrl": "logo_url muss eine http(s)-URL oder das lokal zwischengespeicherte Symbol dieser VM sein",
"discoveryId": "ungültige discovery_id",
"helperSlug": "helper_slug muss ein Slug in Kleinbuchstaben sein (Buchstaben/Ziffern/._-)",
"updateVia": "update_via akzeptiert nur 'docker'",
"dockerDetector": "update_via=docker erfordert einen Detektor docker_label oder docker_exec",
"dockerConflict": "update_via=docker kann nicht mit {field} kombiniert werden",
"updateCommandType": "update_command muss eine Zeichenkette sein",
"updateCommandLength": "update_command überschreitet {max} Zeichen",
"updateCommandNull": "update_command enthält ein Null-Byte",
"updateMethod": "update_method muss none, helper oder custom sein",
"updateCommandRequired": "update_command ist für update_method=custom erforderlich",
"updateCommandOnlyCustom": "update_command ist nur für update_method=custom zulässig",
"helperRequired": "für update_method=helper ist ein unterstützter helper_slug erforderlich",
"dockerUpdater": "An Docker delegierte Apps werden über ihr Image aktualisiert, nicht über einen separaten App-Updater",
"commandRequired": "command_argv ist erforderlich (nicht leere Liste)",
"commandList": "command_argv muss eine nicht leere Liste von Zeichenketten sein",
"commandCount": "command_argv akzeptiert höchstens {max} Einträge",
"commandEntry": "command_argv[{index}] muss eine nicht leere Zeichenkette sein",
"commandEntryLength": "command_argv[{index}] überschreitet {max} Zeichen",
"commandEntryControl": "command_argv[{index}] enthält ein unzulässiges Steuerzeichen",
"binaryArgsList": "binary_args muss eine Liste von Zeichenketten sein",
"binaryArgsCount": "binary_args akzeptiert höchstens {max} Einträge",
"binaryArgEntry": "binary_args[{index}] muss eine nicht leere Zeichenkette sein",
"binaryArgLength": "binary_args[{index}] überschreitet {max} Zeichen",
"binaryArgControl": "binary_args[{index}] enthält ein unzulässiges Steuerzeichen",
"portsList": "ports muss eine Liste von {port, description, scheme} sein",
"portObject": "ports[{index}] muss ein Objekt sein",
"portRequired": "ports[{index}].port ist erforderlich",
"portInteger": "ports[{index}].port muss eine Ganzzahl sein",
"portRange": "ports[{index}].port muss zwischen 1 und 65535 liegen",
"portDuplicate": "Port {port} kommt für diese App mehrfach vor",
"portDescription": "ports[{index}].description enthält ungültige Zeichen",
"portScheme": "ports[{index}].scheme muss 'http' oder 'https' sein",
"portPath": "ports[{index}].web_path muss ein gültiger URL-Pfad sein (max. 255 Zeichen)",
"portLogo": "ports[{index}].logo_url muss eine http(s)-URL sein (max. 512 Zeichen)",
"portCategory": "ports[{index}].category enthält ungültige Zeichen oder ist zu lang",
"portCustomUrl": "ports[{index}].custom_url muss eine http(s)-URL sein (max. 512 Zeichen)",
"tagRegexLength": "tag_regex überschreitet 512 Zeichen",
"persistPermission": "Sidecar konnte nicht gespeichert werden (Berechtigung?)",
"persist": "Sidecar konnte nicht gespeichert werden",
"noApps": "für diese vmid sind keine Apps registriert",
"appMissing": "app_id '{appId}' nicht gefunden",
"appNotFound": "App nicht gefunden"
}
},
"statusFilter": {
"ariaLabel": "Virtuelle Maschinen und Container filtern",
@@ -2294,6 +2417,42 @@
"updateManyDescription": "Wiederholen Sie die Nachinstallationsfunktionen von {count} nacheinander.",
"legacySourceTitle": "Legacy-Optimierung – klicken Sie, um die Quelle anzuzeigen",
"sourceTitle": "Klicken Sie hier, um den Quellcode anzuzeigen",
"sourceDialogTitle": "Quellcode: {name}",
"sourceLoading": "Quellcode wird geladen…",
"sourceUnavailable": "Der Quellcode ist nicht verfügbar.",
"sourceLoadFailed": "Der Quellcode konnte nicht geladen werden. Versuchen Sie es erneut.",
"sourceFunctionMissing": "Die Funktion wurde in den verfügbaren Skripten nicht gefunden.",
"sourceUnknownTool": "Unbekanntes Tool: {tool}",
"sourceNoMapping": "Dem Tool {tool} ist keine Funktion zugeordnet.",
"sourceToolNames": {
"subscription_banner": "Entfernung des Abonnementbanners",
"time_sync": "Zeitsynchronisierung",
"apt_languages": "APT-Sprachdateien überspringen",
"journald": "Journald-Optimierung",
"logrotate": "Logrotate-Optimierung",
"system_limits": "Erhöhung der Systemlimits",
"memory_settings": "Optimierung der Speichereinstellungen",
"zram_swap": "ZRAM-Swap-Profil",
"kernel_panic": "Konfiguration bei Kernel Panic",
"apt_ipv4": "APT IPv4 erzwingen",
"kexec": "kexec für schnelle Neustarts",
"rpc": "RPC / rpcbind deaktivieren",
"motd": "Benutzerdefiniertes MOTD-Banner",
"system_utils": "Systemdienstprogramme",
"network_optimization": "Netzwerkoptimierungen",
"bashrc_custom": "Bashrc-Anpassung",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (SSD-Schutz)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Korrekturen für AMD-CPUs (Ryzen/EPYC)",
"persistent_network": "Persistente Netzwerkschnittstellen festlegen",
"vfio_iommu": "VFIO/IOMMU-Passthrough",
"lvm_repair": "Reparatur der LVM-PV-Header",
"repo_cleanup": "Repository-Bereinigung",
"proxmox_repos": "Proxmox-APT-Repositorys",
"entropy": "Entropie-Erzeugung (haveged)"
},
"legacy": "Vermächtnis",
"updateToolTitle": "Aktualisieren Sie {name} auf v{version}",
"availableUpdates": "Verfügbare Updates",
@@ -4820,6 +4979,124 @@
"cancel": "Abbrechen",
"save": "Aufbewahrung speichern",
"useDefault": "Proxmox-Standard verwenden (keine Richtlinie für diesen Speicher)"
},
"apiErrors": {
"requestFailed": "Die Anfrage ist fehlgeschlagen.",
"absolutePath": "der Pfad muss absolut sein",
"pathConfigured": "dieser Pfad ist bereits konfiguriert",
"pathRequired": "der Pfad ist erforderlich",
"pathQuery": "der Abfrageparameter path ist erforderlich",
"pathNotFound": "Pfad nicht gefunden",
"invalidJobId": "ungültige Auftrags-ID",
"jobNotFound": "Auftrag nicht gefunden",
"runnerMissing": "Ausführungsskript nicht installiert",
"noRestore": "kein Wiederherstellungsstatus zum Verwerfen vorhanden",
"invalidFile": "ungültiger Dateiname",
"notFound": "nicht gefunden",
"logOutside": "Protokollpfad außerhalb des zulässigen Verzeichnisses",
"targetsList": "targets muss eine Liste sein",
"emptyExpression": "leerer Ausdruck",
"invalidId": "ungültige ID (Buchstaben, Ziffern, _ oder - verwenden)",
"backend": "backend muss pbs, local oder borg sein",
"remoteBackend": "backend muss pbs oder borg sein",
"remoteBackendQuoted": "backend muss 'pbs' oder 'borg' sein",
"profileMode": "profile_mode muss default oder custom sein",
"borgAttached": "borg kann nicht angehängt werden (PVE vzdump schreibt nur auf pbs/local-Speicher)",
"borgAttachedLong": "borg ist kein gültiges Backend für angehängte Aufträge (PVE vzdump schreibt nur auf pbs/local-Speicher)",
"parentRequired": "pve_parent_job_id und pve_storage sind für attached erforderlich",
"absolutePaths": "paths muss eine Liste absoluter Pfade sein",
"customPath": "das benutzerdefinierte Profil benötigt mindestens einen Pfad",
"defaultPaths": "die Pfade des Standardprofils konnten nicht ermittelt werden",
"retentionObject": "retention muss ein Objekt sein",
"pbsRepository": "pbs_repository ist erforderlich",
"pbsEncryption": "pbs_encrypt_mode muss 'none', 'new' oder 'existing' sein",
"pbsKeyFailed": "die PBS-Verschlüsselungsschlüsseldatei konnte nicht erstellt werden",
"pbsClientMissing": "die PBS-Verschlüsselungsschlüsseldatei konnte nicht erstellt werden (ist proxmox-backup-client installiert?)",
"borgRepository": "borg_repo ist erforderlich",
"borgEncryption": "borg_encrypt_mode muss einer der folgenden Werte sein: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode muss einer der folgenden Werte sein: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase ist erforderlich, wenn die Verschlüsselung aktiviert ist (am Ziel speichern oder in der Anfrage übergeben)",
"passphrase": "die Passphrase ist erforderlich, wenn die Verschlüsselung aktiviert ist",
"password": "das Passwort ist erforderlich",
"deviceConfirmation": "confirm_device muss exakt mit device übereinstimmen (Tippfehler-/Sicherheitsprüfung)",
"invalidDevice": "ungültiger Gerätepfad",
"absoluteMountpoint": "absoluter Einhängepunkt erforderlich",
"archiveLayout": "Archivstruktur nicht erkannt – kein rootfs/ gefunden",
"archiveMissing": "Archiv nicht gefunden",
"archiveNotFile": "der Archivpfad ist keine reguläre Datei",
"invalidName": "ungültiger Name",
"invalidStaging": "ungültiger staging_path",
"jobRename": "der Auftragsname kann nicht geändert werden (zum Umbenennen löschen und neu erstellen)",
"absoluteKey": "key_path muss absolut sein",
"mode": "mode muss \"local\" oder \"ssh\" sein",
"nameRequired": "der Name ist erforderlich (Buchstaben, Ziffern, _ oder -)",
"noLog": "für diesen Auftrag ist noch kein Protokoll verfügbar",
"noPbsConfig": "keine Datei mit manuellen PBS-Konfigurationen",
"outputMissing": "Ausgabedatei fehlt",
"parserJson": "die Ausgabe des Parsers war kein gültiges JSON",
"restoreJson": "die Ausgabe von run_restore war kein gültiges JSON",
"repoRequired": "repo ist erforderlich",
"repoSnapshot": "repo_name und snapshot sind erforderlich",
"borgRepoSnapshot": "repo_name und snapshot sind für borg erforderlich",
"pbsRepoSnapshot": "repo_name und snapshot sind für pbs erforderlich",
"repoName": "repo_name ist erforderlich",
"restoreMissing": "die Wiederherstellungswerkzeuge sind auf diesem Host nicht installiert",
"serverDatastore": "server und datastore sind erforderlich",
"snapshotRequired": "snapshot ist erforderlich",
"source": "source muss pbs, borg oder local sein",
"sshPort": "ssh_port muss eine Ganzzahl zwischen 1 und 65535 sein",
"sshRequired": "ssh_user, ssh_host und ssh_remote_path sind für den ssh-Modus erforderlich",
"repositoryPath": "das Ziel hat keinen Repository-Pfad",
"taskExpired": "Aufgabe nicht gefunden oder abgelaufen",
"invalidExpression": "ungültiger Ausdruck",
"noReport": "kein Bericht ausgegeben",
"parseManifestFailed": "parse_manifest wurde mit einem Fehlercode beendet",
"jobExists": "Auftrag existiert bereits: {value}",
"pathMissing": "Pfad existiert auf diesem Host nicht: {value}",
"deviceMissing": "Gerät existiert nicht: {value}",
"mountpoint": "kein Einhängepunkt: {value}",
"localArchiveMissing": "lokales Archiv nicht gefunden: {value}",
"borgDestination": "Borg-Ziel '{value}' nicht gefunden",
"pbsDestination": "PBS-Ziel '{value}' nicht gefunden",
"pbsPassword": "kein Passwort für das PBS-Repo '{value}' verfügbar",
"borgRepoMissing": "Borg-Repo \"{value}\" nicht konfiguriert",
"pbsRepoMissing": "PBS-Repo \"{value}\" nicht konfiguriert",
"borgTargetMissing": "Borg-Ziel nicht gefunden: {value}",
"pbsTargetMissing": "PBS-Repo nicht gefunden: {value}",
"pveJobMissing": "PVE-vzdump-Auftrag nicht gefunden: {value}",
"statePersist": "Status kann nicht gespeichert werden: {value}",
"sshDirectory": "SSH-Verzeichnis kann nicht vorbereitet werden: {value}",
"logRead": "Protokoll kann nicht gelesen werden: {value}",
"envUpdate": ".env konnte nicht aktualisiert werden: {value}",
"pbsConfigWrite": "PBS-Konfiguration konnte nicht gespeichert werden: {value}",
"persist": "Speichern fehlgeschlagen: {value}",
"archiveRemove": "Archiv konnte nicht entfernt werden: {value}",
"start": "Start fehlgeschlagen: {value}",
"borgTargetsUpdate": "borg-targets.txt konnte nicht aktualisiert werden: {value}",
"pbsTargetsUpdate": "pbs-manual-configs.txt konnte nicht aktualisiert werden: {value}",
"unitsUpdate": "systemd-Units konnten nicht aktualisiert werden: {value}",
"borgTargetsWrite": "borg-targets.txt konnte nicht geschrieben werden: {value}",
"jobWrite": "Auftragsdateien konnten nicht geschrieben werden: {value}",
"localTargetWrite": "local-target.conf konnte nicht geschrieben werden: {value}",
"unitsWrite": "systemd-Units konnten nicht geschrieben werden: {value}",
"format": "Formatieren fehlgeschlagen: {value}",
"calendar": "ungültiger on_calendar-Wert: {value}",
"mount": "Einhängen fehlgeschlagen: {value}",
"parserInvocation": "Aufruf des Parsers fehlgeschlagen: {value}",
"preflightInvocation": "Aufruf der Vorabprüfung fehlgeschlagen: {value}",
"publicKeyMissing": "öffentlicher Schlüssel nach der Erzeugung nicht gefunden: {value}",
"retentionInteger": "retention.{value} muss eine Ganzzahl sein",
"retentionNegative": "retention.{value} darf nicht negativ sein",
"keygen": "ssh-keygen fehlgeschlagen: {value}",
"systemctl": "systemctl fehlgeschlagen: {value}",
"unmount": "umount fehlgeschlagen: {value}",
"taskIncomplete": "die Aufgabe ist im Status \"{value}\", nicht abgeschlossen",
"unknownKind": "unbekannte Art: {value}",
"unknownMode": "unbekannter Modus \"{value}\"",
"defaultRemove": "{value} ist der integrierte Standard und kann nicht entfernt werden",
"defaultAdd": "{value} ist der integrierte Standard; er muss nicht hinzugefügt werden",
"keyImport": "Unter {value} ist keine PBS-Schlüsseldatei installiert. Importieren Sie zuerst die Schlüsseldatei.",
"storageMismatch": "Speicher des PVE-Auftrags stimmt nicht überein (Auftrag verwendet {value}, Nutzdaten geben {other} an)"
}
},
"healthStatus": {
@@ -5320,6 +5597,7 @@
"websocketError": "Es ist ein WebSocket-Fehler aufgetreten",
"connectionClosed": "Verbindung geschlossen",
"completedSuccessfully": "Skript erfolgreich abgeschlossen",
"cancelled": "Vorgang abgebrochen.",
"completedWithError": "Skript mit Fehlercode {code} beendet",
"yes": "Ja",
"yourInput": "Ihr Beitrag:",
@@ -7311,7 +7589,7 @@
"signInWith": "Anmelden mit",
"methodPassword": "Benutzer und Passwort",
"methodToken": "API-Token",
"username": "Benutzer (benutzer@realm)",
"username": "Benutzer",
"password": "Passwort",
"passwordHelp": "Das Passwort wird nur einmal verwendet, um ein schreibgeschütztes API-Token (Rolle Audit) für diesen Monitor zu erstellen. Gespeichert wird nur dieses Token, nie das Passwort. Aktivieren Sie zuerst die Monitor-Authentifizierung.",
"cancel": "Abbrechen",
@@ -7324,10 +7602,10 @@
"editConnection": "Zu bearbeitende Verbindung",
"add": "Neue Verbindung hinzufügen",
"name": "Name",
"url": "HTTPS-URL (für den nativen PBS-Port :8007 angeben)",
"token_id": "API-Token-ID (user@realm!token)",
"token_secret": "API-Token-Geheimnis (leer lassen, um das gespeicherte Geheimnis beizubehalten)",
"fingerprint": "SHA256-Zertifikatfingerabdruck (optional)",
"url": "HTTPS-Adresse",
"token_id": "API-Token-ID",
"token_secret": "API-Token-Geheimnis",
"fingerprint": "SHA256-Fingerabdruck (optional)",
"tlsHelp": "Lassen Sie den Fingerabdruck für die normale Prüfung von Zertifizierungsstelle und Hostname leer. Kopieren Sie bei einem selbstsignierten Zertifikat dessen SHA256-Fingerabdruck aus der PBS-Konsole oder einem anderen vertrauenswürdigen Kanal. Der Fingerabdruck wird vor der Übermittlung von Zugangsdaten geprüft.",
"saved": "PBS-Verbindung geprüft und gespeichert.",
"saveFailed": "PBS-Verbindung konnte nicht gespeichert werden.",
@@ -7369,7 +7647,85 @@
"estimatedFull": "Voraussichtlich voll",
"estimatedFullUnknown": "Noch nicht genügend Daten",
"duration": "Dauer",
"noJobs": "Es sind keine Synchronisierungs-, Überprüfungs- oder Löschzeitpläne konfiguriert."
"noJobs": "Es sind keine Synchronisierungs-, Überprüfungs- oder Löschzeitpläne konfiguriert.",
"errors": {
"configRead": "Die gespeicherten PBS-Verbindungen konnten nicht gelesen werden. Prüfen Sie die Konfigurationsdatei und die Berechtigungen.",
"invalidConfig": "Die Einstellungen der PBS-Verbindung sind ungültig. Prüfen Sie das Formular und versuchen Sie es erneut.",
"invalidField": "Ein Verbindungsfeld ist ungültig oder zu lang.",
"invalidUrl": "Verwenden Sie eine HTTPS-Adresse des PBS ohne eingebettete Zugangsdaten, Abfrageparameter oder Fragment.",
"nameRequired": "Geben Sie einen Servernamen mit höchstens 128 Zeichen ein.",
"tokenIdFormat": "Geben Sie die PBS-API-Token-ID im Format user@realm!token ein.",
"tokenSecret": "Geben Sie ein gültiges API-Token-Geheimnis ohne Leerzeichen oder Steuerzeichen ein.",
"invalidTokenId": "Die API-Token-ID darf nur ASCII-Zeichen enthalten.",
"invalidFingerprint": "Geben Sie den SHA256-Fingerabdruck des Zertifikats ein: 64 Hexadezimalzeichen, optional durch Doppelpunkte getrennt.",
"fingerprintMismatch": "Das PBS-Zertifikat stimmt nicht mit dem Fingerabdruck überein. Es wurden keine Zugangsdaten gesendet.",
"tokenRejected": "PBS hat das API-Token oder seine Berechtigungen abgelehnt. Prüfen Sie die ACLs sowohl des Benutzers als auch des Tokens.",
"invalidResponse": "PBS hat eine unerwartete Antwort zurückgegeben. Weiterleitungen werden nicht verfolgt.",
"invalidJson": "PBS hat ein ungültiges JSON-Dokument zurückgegeben.",
"connectFailed": "Über verifiziertes HTTPS konnte keine Verbindung zu PBS hergestellt werden. Prüfen Sie Adresse, Zertifikat und Dienst.",
"invalidId": "Die ID der PBS-Verbindung ist ungültig. Laden Sie die Verbindungsliste neu.",
"invalidDatastores": "PBS hat eine ungültige Datenspeicherliste zurückgegeben.",
"connectionLimit": "Es können höchstens {count} PBS-Verbindungen konfiguriert werden.",
"username": "Geben Sie den PBS-Benutzer im Format user@realm ein, zum Beispiel root@pam.",
"password": "Geben Sie das Passwort des PBS-Benutzers ein.",
"loginRejected": "PBS hat den Benutzernamen oder das Passwort abgelehnt.",
"invalidLogin": "PBS hat eine ungültige Anmeldeantwort zurückgegeben.",
"tokenCreateDenied": "Dieser PBS-Benutzer kann keine API-Tokens erstellen.",
"invalidToken": "PBS hat ein ungültiges API-Token zurückgegeben.",
"auditRoleDenied": "Dieser PBS-Benutzer kann die Rolle Audit nicht vergeben. Melden Sie sich als root@pam oder als anderer Administrator an.",
"notFound": "Die PBS-Verbindung wurde nicht gefunden. Laden Sie die Verbindungsliste neu.",
"readFailed": "PBS-Daten konnten nicht gelesen werden. Prüfen Sie den Dienst und die Berechtigungen.",
"invalidList": "PBS hat eine ungültige Datenliste zurückgegeben.",
"localUnavailable": "PBS ist auf diesem Host nicht verfügbar.",
"authRequired": "Aktivieren Sie die lokale Authentifizierung im Monitor, bevor Sie entfernte PBS-Verbindungen verwalten.",
"datastoreOffline": "Der Datenspeicher ist offline, im Wartungsmodus oder sein zugrunde liegendes Gerät ist nicht eingehängt.",
"datastoreUnavailable": "Das Dateisystem des Datenspeichers ist nicht verfügbar."
},
"taskTypes": {
"backup": "Backup",
"restore": "Wiederherstellung",
"verify": "Verifizierung",
"verifyGroup": "Verifizierung der Backup-Gruppe",
"verifySnapshot": "Verifizierung des Snapshots",
"prune": "Bereinigung",
"garbageCollection": "Garbage Collection",
"sync": "Synchronisierung",
"updateCheck": "Nach Updates suchen",
"terminal": "Terminal",
"createDatastore": "Datenspeicher erstellen",
"other": "Andere Aufgabe ({type})"
},
"states": {
"ok": "OK",
"unknown": "Unbekannt",
"warnings": "Warnungen: {count}",
"warning": "Warnung",
"error": "Fehler"
},
"serviceStates": {
"active": "Aktiv",
"inactive": "Inaktiv",
"failed": "Fehlgeschlagen",
"activating": "Wird gestartet",
"deactivating": "Wird gestoppt",
"unknown": "Unbekannt"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} T"
},
"urlHelp": "Geben Sie :8007 für den Standard-PBS-Port an, zum Beispiel https://pbs.example:8007.",
"usernameHelp": "Verwenden Sie user@realm, zum Beispiel root@pam.",
"tokenIdHelp": "Verwenden Sie user@realm!token, zum Beispiel monitor@pbs!proxmenux.",
"tokenSecretHelp": "Geben Sie das Geheimnis dieses API-Tokens ein.",
"tokenSecretKeepHelp": "Leer lassen, um das gespeicherte Token-Geheimnis beizubehalten.",
"removeFailed": "Die PBS-Verbindung konnte nicht entfernt werden.",
"savedRefreshFailed": "Die PBS-Verbindung wurde gespeichert, aber die Verbindungsliste konnte nicht aktualisiert werden. Laden Sie die Seite neu, bevor Sie eine weitere Änderung vornehmen.",
"removedRefreshFailed": "Die PBS-Verbindung wurde entfernt, aber die Verbindungsliste konnte nicht aktualisiert werden. Laden Sie die Seite neu.",
"technicalDetails": "Technische Details",
"datastoreType": "Datenspeicher"
},
"monitorLinks": {
"groups": {
@@ -7408,6 +7764,17 @@
"addSelected": "Auswahl hinzufügen",
"pbsWebInterface": "PBS-Weboberfläche",
"changeAddress": "Adresse ändern",
"cancel": "Abbrechen"
"cancel": "Abbrechen",
"errors": {
"addressRequired": "Geben Sie die Adresse des Monitors ein.",
"invalidAddress": "Die Adresse muss mit http:// oder https:// beginnen und einen Host angeben.",
"addressCredentials": "Die Adresse darf keinen Benutzer und kein Passwort enthalten.",
"invalidPort": "Der Port der Adresse ist ungültig.",
"invalidRequest": "Die Anfrage ist ungültig. Laden Sie die Seite neu und versuchen Sie es erneut.",
"limit": "Es können höchstens {count} Knoten hinzugefügt werden.",
"duplicate": "Diese Adresse ist bereits in der Liste.",
"notFound": "Der Knoten wurde nicht gefunden. Laden Sie die Liste neu und versuchen Sie es erneut.",
"detectedRemove": "Nur manuell hinzugefügte Knoten können entfernt werden. Erkannte Knoten können ausgeblendet werden."
}
}
}
+351 -9
View File
@@ -14,7 +14,7 @@
"signInWith": "Sign in with",
"methodPassword": "User and password",
"methodToken": "API token",
"username": "User (user@realm)",
"username": "User",
"password": "Password",
"passwordHelp": "The password is used once to create a read-only API token (Audit role) for this Monitor. Only that token is saved, never the password. Enable Monitor authentication first.",
"cancel": "Cancel",
@@ -27,10 +27,15 @@
"editConnection": "Connection to edit",
"add": "Add a new connection",
"name": "Name",
"url": "HTTPS URL (include :8007 for the native PBS port)",
"token_id": "API token ID (user@realm!token)",
"token_secret": "API token secret (leave empty to keep the stored secret)",
"fingerprint": "SHA256 certificate fingerprint (optional)",
"url": "HTTPS address",
"urlHelp": "Include :8007 for the default PBS port, for example https://pbs.example:8007.",
"usernameHelp": "Use user@realm, for example root@pam.",
"tokenIdHelp": "Use user@realm!token, for example monitor@pbs!proxmenux.",
"tokenSecretHelp": "Enter the secret of this API token.",
"tokenSecretKeepHelp": "Leave empty to keep the saved token secret.",
"token_id": "API token ID",
"token_secret": "API token secret",
"fingerprint": "SHA256 fingerprint (optional)",
"tlsHelp": "Leave the fingerprint empty for normal CA and hostname verification. For a self-signed certificate, copy its SHA256 fingerprint from the PBS console or another trusted channel. The pin is checked before sending credentials.",
"saved": "PBS connection verified and saved.",
"saveFailed": "Could not save the PBS connection.",
@@ -72,7 +77,55 @@
"estimatedFull": "Estimated full",
"estimatedFullUnknown": "Not enough data yet",
"duration": "Duration",
"noJobs": "No sync, verification or prune schedules are configured."
"noJobs": "No sync, verification or prune schedules are configured.",
"removeFailed": "Could not remove the PBS connection.",
"savedRefreshFailed": "The PBS connection was saved, but the connection list could not be refreshed. Reload the page before trying another change.",
"removedRefreshFailed": "The PBS connection was removed, but the connection list could not be refreshed. Reload the page.",
"technicalDetails": "Technical details",
"datastoreType": "Datastore",
"errors": {
"configRead": "Could not read the saved PBS connections. Check the configuration file and permissions.",
"invalidConfig": "The PBS connection settings are not valid. Check the form and try again.",
"invalidField": "A connection field is not valid or is too long.",
"invalidUrl": "Use an HTTPS PBS address without embedded credentials, query parameters or a fragment.",
"nameRequired": "Enter a server name of at most 128 characters.",
"tokenIdFormat": "Enter the PBS API token ID in user@realm!token format.",
"tokenSecret": "Enter a valid API token secret without spaces or control characters.",
"invalidTokenId": "The API token ID must contain only ASCII characters.",
"invalidFingerprint": "Enter the certificate's SHA256 fingerprint: 64 hexadecimal characters, optionally separated by colons.",
"fingerprintMismatch": "The PBS certificate does not match the fingerprint. No credentials were sent.",
"tokenRejected": "PBS rejected the API token or its permissions. Check the ACLs of both the user and the token.",
"invalidResponse": "PBS returned an unexpected response. Redirects are not followed.",
"invalidJson": "PBS returned an invalid JSON document.",
"connectFailed": "Could not connect to PBS over verified HTTPS. Check the address, certificate and service.",
"invalidId": "The PBS connection ID is not valid. Reload the connection list.",
"invalidDatastores": "PBS returned an invalid datastore list.",
"connectionLimit": "At most {count} PBS connections may be configured.",
"username": "Enter the PBS user in user@realm format, for example root@pam.",
"password": "Enter the PBS user's password.",
"loginRejected": "PBS rejected the username or password.",
"invalidLogin": "PBS returned an invalid sign-in response.",
"tokenCreateDenied": "This PBS user cannot create API tokens.",
"invalidToken": "PBS returned an invalid API token.",
"auditRoleDenied": "This PBS user cannot grant the Audit role. Sign in as root@pam or another administrator.",
"notFound": "The PBS connection was not found. Reload the connection list.",
"readFailed": "Could not read PBS data. Check its service and permissions.",
"invalidList": "PBS returned an invalid data list.",
"localUnavailable": "PBS is not available on this host.",
"authRequired": "Enable local authentication in Monitor before managing remote PBS connections.",
"datastoreOffline": "The datastore is offline, in maintenance mode or its backing device is not mounted.",
"datastoreUnavailable": "The datastore filesystem is not available."
},
"taskTypes": {
"backup": "Backup", "restore": "Restore", "verify": "Verification",
"verifyGroup": "Backup group verification", "verifySnapshot": "Snapshot verification",
"prune": "Prune", "garbageCollection": "Garbage collection", "sync": "Synchronization",
"updateCheck": "Check for updates", "terminal": "Terminal", "createDatastore": "Create datastore",
"other": "Other task ({type})"
},
"states": { "ok": "OK", "unknown": "Unknown", "warnings": "Warnings: {count}", "warning": "Warning", "error": "Error" },
"serviceStates": { "active": "Active", "inactive": "Inactive", "failed": "Failed", "activating": "Starting", "deactivating": "Stopping", "unknown": "Unknown" },
"durationUnits": { "seconds": "{count} s", "minutes": "{count} min", "hours": "{count} h", "days": "{count} d" }
},
"app": {
"title": "ProxMenux Monitor",
@@ -1327,9 +1380,11 @@
"update": "Update",
"modify": "Modify",
"recover": "Recover",
"registerRestored": "Register again",
"terminalTitleUpdate": "Update OCI application",
"terminalTitleRecoverPrevious": "Recover the previous OCI installation",
"terminalTitleModify": "Modify OCI application",
"terminalTitleRecover": "Recover restored OCI application",
"terminalTitleRecover": "Register restored OCI applications",
"terminalDescription": "Runs the same process as the ProxMenux menu: OCI manager Apps → Manage installed OCI applications.",
"keepBackup": "Keep the backup taken before updating",
"noKeepBackup": "The backup taken before updating is not kept",
@@ -1560,7 +1615,7 @@
"ociTitle": "OCI image container",
"ociBody": "This container was created from an OCI image outside OCI manager Apps. It is updated by replacing its image, which ProxMenux does not manage for it.",
"ociRestoredTitle": "Restored OCI application",
"ociRestoredBody": "This container was restored from a backup and this host has no record of its application, so it cannot be updated from here yet. Recover registers it again on this host, the same as OCI manager Apps → Manage installed OCI applications.",
"ociRestoredBody": "This container was restored from a backup and this host has no record of its OCI application, so it cannot be updated here yet. Register again opens the registration of restored OCI applications on this node, as in OCI manager Apps → Manage installed OCI applications.",
"dockerImagesTitle": "Docker images",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
@@ -1620,6 +1675,127 @@
"loadFailed": "Could not load the bulk update configuration."
},
"appEditor": {
"confirmRemove": "Remove this application's record from the App tab? This does not uninstall the application.",
"packageExample": "e.g. {package}",
"packageHelpBefore": "Auto-filled from Name. Verify with",
"packageHelpAfter": "inside the CT.",
"binaryHelpBefore": "Absolute path. Find it with",
"binaryHelpOr": "or",
"errors": {
"ociBusy": "an OCI operation on this container is running",
"dpkgMissing": "{package} is not installed via dpkg",
"dpkgFailed": "dpkg-query failed",
"dpkgNoVersion": "no version matched in dpkg output",
"apkFailed": "apk info failed",
"apkNoVersion": "no version matched in apk output",
"fileRead": "could not read {path}",
"fileNoMatch": "file_regex did not match {path}",
"binaryFailed": "binary invocation failed",
"binaryNoVersion": "no version matched in binary output",
"pythonFailed": "python -c importlib.metadata failed",
"pythonNoVersion": "no version matched in Python distribution output",
"dockerImageResolve": "could not resolve the container's installed image",
"dockerInspectFailed": "docker inspect failed",
"dockerLabelMissing": "container has no {label} label",
"dockerLabelMutable": "docker label reports {tag} (mutable tag, not a version)",
"dockerLabelNoVersion": "no version matched in docker label output",
"dockerExecFailed": "docker exec failed",
"dockerExecNoVersion": "no version matched in docker exec output",
"commandEmpty": "command_argv is empty",
"commandFailed": "command failed",
"commandNoVersion": "no version matched in command output",
"unsupportedMethod": "unsupported method: {method}",
"unknownUpstream": "unknown upstream_type: {type}",
"githubNotFound": "{repo}: not found",
"githubRejected": "github rejected the request (403)",
"githubError": "github error {code}",
"noRelease": "no tag / release name in response",
"tagNoMatch": "tag_regex did not match '{tag}'",
"httpError": "http error {code}",
"invalidJson": "invalid JSON response: {detail}",
"jsonPathUnresolved": "json_path '{path}' did not resolve",
"jsonEmpty": "empty value at json_path",
"dockerNoTags": "no tags matched (docker_hub)",
"dockerMovingOnly": "tag_regex matched only moving tags; use Docker image digest tracking",
"dockerHubError": "docker hub error {code}",
"dockerHubJson": "invalid docker hub JSON: {detail}",
"dockerHubEmpty": "no tags returned by docker hub",
"payload": "payload must be a JSON object",
"name": "name is required and must be 1-64 chars of letters/digits/spaces/._+-()/",
"method": "installed_via must be one of: {values} or empty",
"package": "package is required (letters/digits/._+@:/ up to 127 chars)",
"filePath": "file_path is required and must be an absolute path",
"fileRegex": "file_regex is required",
"fileRegexInvalid": "file_regex is not a valid regex: {detail}",
"binaryPath": "binary_path is required and must be an absolute path",
"pythonPath": "python_path is required and must be an absolute path",
"distribution": "distribution is required (PEP 503 name)",
"container": "container_name is required (docker naming rules)",
"label": "label is required (OCI label naming rules)",
"dockerBinary": "binary_path is required (absolute path or bare command)",
"manualVersion": "installed_version is required for the manual method",
"versionLength": "installed_version exceeds {max} chars",
"versionControl": "installed_version contains a forbidden control character",
"installedRegexInvalid": "installed_regex is not a valid regex: {detail}",
"upstreamType": "upstream_type must be one of: {values}",
"tagRegexInvalid": "tag_regex is not a valid regex: {detail}",
"repoRequired": "repo is required for upstream_type=github",
"repoFormat": "repo must be 'owner/name'",
"githubSource": "github_source must be one of: {values}",
"upstreamUrlRequired": "upstream_url is required for upstream_type=http_json",
"upstreamUrl": "upstream_url must be an http(s) URL",
"upstreamUrlLength": "upstream_url exceeds {max} chars",
"jsonPathRequired": "upstream_json_path is required for upstream_type=http_json",
"jsonPathLength": "upstream_json_path exceeds {max} chars",
"jsonPathCharacters": "upstream_json_path uses forbidden characters",
"dockerImageRequired": "docker_image is required for upstream_type=docker_hub",
"dockerImageLength": "docker_image exceeds {max} chars",
"dockerImageFormat": "docker_image must match Docker Hub naming (owner/name or name)",
"healthPath": "health_path must be a valid URL path (max 255 chars)",
"logoUrl": "logo_url must be an http(s) URL or this VM's cached local icon",
"discoveryId": "invalid discovery_id",
"helperSlug": "helper_slug must be a lowercase slug (letters/digits/._-)",
"updateVia": "update_via only accepts 'docker'",
"dockerDetector": "update_via=docker requires a docker_label or docker_exec detector",
"dockerConflict": "update_via=docker cannot be combined with {field}",
"updateCommandType": "update_command must be a string",
"updateCommandLength": "update_command exceeds {max} chars",
"updateCommandNull": "update_command contains a null byte",
"updateMethod": "update_method must be none, helper or custom",
"updateCommandRequired": "update_command is required for update_method=custom",
"updateCommandOnlyCustom": "update_command is only allowed for update_method=custom",
"helperRequired": "a supported helper_slug is required for update_method=helper",
"dockerUpdater": "Docker-delegated apps update through their image, not a separate app updater",
"commandRequired": "command_argv is required (non-empty list)",
"commandList": "command_argv must be a non-empty list of strings",
"commandCount": "command_argv accepts at most {max} entries",
"commandEntry": "command_argv[{index}] must be a non-empty string",
"commandEntryLength": "command_argv[{index}] exceeds {max} chars",
"commandEntryControl": "command_argv[{index}] contains a forbidden control character",
"binaryArgsList": "binary_args must be a list of strings",
"binaryArgsCount": "binary_args accepts at most {max} entries",
"binaryArgEntry": "binary_args[{index}] must be a non-empty string",
"binaryArgLength": "binary_args[{index}] exceeds {max} chars",
"binaryArgControl": "binary_args[{index}] contains a forbidden control character",
"portsList": "ports must be a list of {port, description, scheme}",
"portObject": "ports[{index}] must be an object",
"portRequired": "ports[{index}].port is required",
"portInteger": "ports[{index}].port must be an integer",
"portRange": "ports[{index}].port must be 1-65535",
"portDuplicate": "port {port} appears more than once for this app",
"portDescription": "ports[{index}].description has invalid characters",
"portScheme": "ports[{index}].scheme must be 'http' or 'https'",
"portPath": "ports[{index}].web_path must be a valid URL path (max 255 chars)",
"portLogo": "ports[{index}].logo_url must be an http(s) URL (max 512 chars)",
"portCategory": "ports[{index}].category has invalid characters or is too long",
"portCustomUrl": "ports[{index}].custom_url must be an http(s) URL (max 512 chars)",
"tagRegexLength": "tag_regex exceeds 512 chars",
"persistPermission": "could not persist sidecar (permission?)",
"persist": "could not persist sidecar",
"noApps": "no apps registered for this vmid",
"appMissing": "app_id '{appId}' not found",
"appNotFound": "app not found"
},
"closePanel": "Close panel",
"cancelButton": "Cancel",
"saveButton": "Save",
@@ -2370,6 +2546,42 @@
"updateManyDescription": "Re-running {count} post-install functions in sequence.",
"legacySourceTitle": "Legacy optimization - click to view source",
"sourceTitle": "Click to view source code",
"sourceDialogTitle": "Source code: {name}",
"sourceLoading": "Loading source code…",
"sourceUnavailable": "Source code is not available.",
"sourceLoadFailed": "Could not load the source code. Try again.",
"sourceFunctionMissing": "The function was not found in the available scripts.",
"sourceUnknownTool": "Unknown tool: {tool}",
"sourceNoMapping": "No function is assigned to the tool {tool}.",
"sourceToolNames": {
"subscription_banner": "Subscription Banner Removal",
"time_sync": "Time Synchronization",
"apt_languages": "APT Language Skip",
"journald": "Journald Optimization",
"logrotate": "Logrotate Optimization",
"system_limits": "System Limits Increase",
"memory_settings": "Memory Settings Optimization",
"zram_swap": "ZRAM Swap Profile",
"kernel_panic": "Kernel Panic Configuration",
"apt_ipv4": "APT IPv4 Force",
"kexec": "kexec for quick reboots",
"rpc": "RPC / rpcbind Disable",
"motd": "Custom MOTD Banner",
"system_utils": "System Utilities",
"network_optimization": "Network Optimizations",
"bashrc_custom": "Bashrc Customization",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (SSD Protection)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "AMD CPU (Ryzen/EPYC) fixes",
"persistent_network": "Setting persistent network interfaces",
"vfio_iommu": "VFIO/IOMMU Passthrough",
"lvm_repair": "LVM PV Headers Repair",
"repo_cleanup": "Repository Cleanup",
"proxmox_repos": "Proxmox APT Repositories",
"entropy": "Entropy Generation (haveged)"
},
"legacy": "legacy",
"updateToolTitle": "Update {name} to v{version}",
"availableUpdates": "Available updates",
@@ -4383,6 +4595,124 @@
"unrecoverable": "Without the key, encrypted backups cannot be restored.",
"usesSharedKeyfileAt": "Uses shared keyfile at"
},
"apiErrors": {
"requestFailed": "The request failed.",
"absolutePath": "path must be absolute",
"pathConfigured": "this path is already configured",
"pathRequired": "path is required",
"pathQuery": "path query parameter is required",
"pathNotFound": "path not found",
"invalidJobId": "invalid job id",
"jobNotFound": "job not found",
"runnerMissing": "runner script not installed",
"noRestore": "no restore state to dismiss",
"invalidFile": "invalid file name",
"notFound": "not found",
"logOutside": "log path outside allowed directory",
"targetsList": "targets must be a list",
"emptyExpression": "empty expression",
"invalidId": "invalid id (use letters, digits, _ or -)",
"backend": "backend must be pbs, local or borg",
"remoteBackend": "backend must be pbs or borg",
"remoteBackendQuoted": "backend must be 'pbs' or 'borg'",
"profileMode": "profile_mode must be default or custom",
"borgAttached": "borg cannot be attached (PVE vzdump only writes to pbs/local storages)",
"borgAttachedLong": "borg is not a valid backend for attached jobs (PVE vzdump only writes to pbs/local storages)",
"parentRequired": "pve_parent_job_id and pve_storage are required for attached",
"absolutePaths": "paths must be a list of absolute paths",
"customPath": "custom profile needs at least one path",
"defaultPaths": "could not resolve default profile paths",
"retentionObject": "retention must be an object",
"pbsRepository": "pbs_repository is required",
"pbsEncryption": "pbs_encrypt_mode must be 'none', 'new' or 'existing'",
"pbsKeyFailed": "failed to create PBS encryption keyfile",
"pbsClientMissing": "failed to create PBS encryption keyfile (is proxmox-backup-client installed?)",
"borgRepository": "borg_repo is required",
"borgEncryption": "borg_encrypt_mode must be one of: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode must be one of: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase is required when encryption is enabled (save it on the destination or pass one in the request)",
"passphrase": "passphrase is required when encryption is enabled",
"password": "password is required",
"deviceConfirmation": "confirm_device must match device exactly (typo / safety check)",
"invalidDevice": "invalid device path",
"absoluteMountpoint": "absolute mountpoint required",
"archiveLayout": "archive layout not recognized — no rootfs/ found",
"archiveMissing": "archive not found",
"archiveNotFile": "archive path is not a regular file",
"invalidName": "invalid name",
"invalidStaging": "invalid staging_path",
"jobRename": "job name cannot be changed (delete + recreate to rename)",
"absoluteKey": "key_path must be absolute",
"mode": "mode must be \"local\" or \"ssh\"",
"nameRequired": "name is required (letters, digits, _ or -)",
"noLog": "no log available for this job yet",
"noPbsConfig": "no manual PBS configs file",
"outputMissing": "output file missing",
"parserJson": "parser output was not valid JSON",
"restoreJson": "run_restore output was not valid JSON",
"repoRequired": "repo is required",
"repoSnapshot": "repo_name and snapshot are required",
"borgRepoSnapshot": "repo_name and snapshot are required for borg",
"pbsRepoSnapshot": "repo_name and snapshot are required for pbs",
"repoName": "repo_name is required",
"restoreMissing": "restore tooling not installed on this host",
"serverDatastore": "server and datastore are required",
"snapshotRequired": "snapshot is required",
"source": "source must be pbs, borg, or local",
"sshPort": "ssh_port must be an integer between 1 and 65535",
"sshRequired": "ssh_user, ssh_host and ssh_remote_path are required for ssh mode",
"repositoryPath": "target has no repository path",
"taskExpired": "task not found or expired",
"invalidExpression": "invalid expression",
"noReport": "no report emitted",
"parseManifestFailed": "parse_manifest exited non-zero",
"jobExists": "job already exists: {value}",
"pathMissing": "path does not exist on this host: {value}",
"deviceMissing": "device does not exist: {value}",
"mountpoint": "not a mountpoint: {value}",
"localArchiveMissing": "local archive not found: {value}",
"borgDestination": "Borg destination '{value}' not found",
"pbsDestination": "PBS destination '{value}' not found",
"pbsPassword": "no password available for PBS repo '{value}'",
"borgRepoMissing": "Borg repo \"{value}\" not configured",
"pbsRepoMissing": "PBS repo \"{value}\" not configured",
"borgTargetMissing": "Borg target not found: {value}",
"pbsTargetMissing": "PBS repo not found: {value}",
"pveJobMissing": "PVE vzdump job not found: {value}",
"statePersist": "cannot persist state: {value}",
"sshDirectory": "cannot prepare ssh dir: {value}",
"logRead": "cannot read log: {value}",
"envUpdate": "could not update .env: {value}",
"pbsConfigWrite": "failed to persist PBS config: {value}",
"persist": "failed to persist: {value}",
"archiveRemove": "failed to remove archive: {value}",
"start": "failed to start: {value}",
"borgTargetsUpdate": "failed to update borg-targets.txt: {value}",
"pbsTargetsUpdate": "failed to update pbs-manual-configs.txt: {value}",
"unitsUpdate": "failed to update systemd units: {value}",
"borgTargetsWrite": "failed to write borg-targets.txt: {value}",
"jobWrite": "failed to write job files: {value}",
"localTargetWrite": "failed to write local-target.conf: {value}",
"unitsWrite": "failed to write systemd units: {value}",
"format": "format failed: {value}",
"calendar": "invalid on_calendar: {value}",
"mount": "mount failed: {value}",
"parserInvocation": "parser invocation failed: {value}",
"preflightInvocation": "preflight invocation failed: {value}",
"publicKeyMissing": "public key not found after generation: {value}",
"retentionInteger": "retention.{value} must be an integer",
"retentionNegative": "retention.{value} must be non-negative",
"keygen": "ssh-keygen failed: {value}",
"systemctl": "systemctl failed: {value}",
"unmount": "umount failed: {value}",
"taskIncomplete": "task is in state \"{value}\", not completed",
"unknownKind": "unknown kind: {value}",
"unknownMode": "unknown mode \"{value}\"",
"defaultRemove": "{value} is the built-in default and cannot be removed",
"defaultAdd": "{value} is the built-in default; no need to add it",
"keyImport": "No PBS keyfile is installed at {value}. Import the keyfile first.",
"storageMismatch": "PVE job storage mismatch (job uses {value}, payload says {other})"
},
"errors": {
"backendNoStagingPath": "Backend did not return a staging path.",
"deleteFailed": "Delete failed: {error}",
@@ -5463,6 +5793,7 @@
"websocketError": "WebSocket error occurred",
"connectionClosed": "Connection closed",
"completedSuccessfully": "Script completed successfully",
"cancelled": "Operation cancelled.",
"completedWithError": "Script finished with error code {code}",
"yes": "Yes",
"yourInput": "Your input:",
@@ -6684,7 +7015,18 @@
"addSelected": "Add selected",
"pbsWebInterface": "PBS web interface",
"changeAddress": "Change address",
"cancel": "Cancel"
"cancel": "Cancel",
"errors": {
"addressRequired": "Enter the address of the Monitor.",
"invalidAddress": "The address must start with http:// or https:// and name a host.",
"addressCredentials": "The address must not contain a user or password.",
"invalidPort": "The port of the address is not valid.",
"invalidRequest": "The request is not valid. Reload the page and try again.",
"limit": "At most {count} nodes can be added.",
"duplicate": "This address is already in the list.",
"notFound": "The node was not found. Reload the list and try again.",
"detectedRemove": "Only nodes added by hand can be removed. Detected nodes can be hidden."
}
},
"apiErrors": {
"http": "API request failed: {status} {statusText}",
+375 -8
View File
@@ -14,7 +14,7 @@
"signInWith": "Iniciar sesión con",
"methodPassword": "Usuario y contraseña",
"methodToken": "Token API",
"username": "Usuario (usuario@realm)",
"username": "Usuario",
"password": "Contraseña",
"passwordHelp": "La contraseña se usa una sola vez para crear un token API de solo lectura (rol Audit) para este Monitor. Solo se guarda ese token, nunca la contraseña. Activa antes la autenticación del Monitor.",
"cancel": "Cancelar",
@@ -27,10 +27,10 @@
"editConnection": "Conexión que deseas editar",
"add": "Añadir una conexión",
"name": "Nombre",
"url": "URL HTTPS (incluye :8007 para el puerto nativo de PBS)",
"token_id": "ID del token API (usuario@realm!token)",
"token_secret": "Secreto del token API (vacío para conservar el guardado)",
"fingerprint": "Huella SHA256 del certificado (opcional)",
"url": "Dirección HTTPS",
"token_id": "ID del token API",
"token_secret": "Secreto del token API",
"fingerprint": "Huella SHA256 (opcional)",
"tlsHelp": "Deja la huella vacía para verificar la CA y el nombre del servidor. Si el certificado es autofirmado, copia su huella SHA256 desde la consola de PBS u otro canal fiable. Se verifica antes de enviar las credenciales.",
"saved": "Conexión PBS verificada y guardada.",
"saveFailed": "No se pudo guardar la conexión PBS.",
@@ -72,7 +72,85 @@
"estimatedFull": "Lleno previsto",
"estimatedFullUnknown": "Aún no hay datos suficientes",
"duration": "Duración",
"noJobs": "No hay programaciones de sincronización, verificación ni prune."
"noJobs": "No hay programaciones de sincronización, verificación ni prune.",
"errors": {
"configRead": "No se pudieron leer las conexiones PBS guardadas. Revisa el archivo de configuración y los permisos.",
"invalidConfig": "La configuración de la conexión PBS no es válida. Revisa el formulario y vuelve a intentarlo.",
"invalidField": "Un campo de la conexión no es válido o es demasiado largo.",
"invalidUrl": "Usa una dirección HTTPS de PBS sin credenciales incrustadas, parámetros de consulta ni fragmento.",
"nameRequired": "Introduce un nombre de servidor de 128 caracteres como máximo.",
"tokenIdFormat": "Introduce el ID del token API de PBS con el formato usuario@realm!token.",
"tokenSecret": "Introduce un secreto de token API válido, sin espacios ni caracteres de control.",
"invalidTokenId": "El ID del token API solo puede contener caracteres ASCII.",
"invalidFingerprint": "Introduce la huella SHA256 del certificado: 64 caracteres hexadecimales, opcionalmente separados por dos puntos.",
"fingerprintMismatch": "El certificado de PBS no coincide con la huella. No se ha enviado ninguna credencial.",
"tokenRejected": "PBS ha rechazado el token API o sus permisos. Revisa las ACL del usuario y del token.",
"invalidResponse": "PBS ha devuelto una respuesta inesperada. No se siguen las redirecciones.",
"invalidJson": "PBS ha devuelto un documento JSON no válido.",
"connectFailed": "No se pudo conectar con PBS mediante HTTPS verificado. Revisa la dirección, el certificado y el servicio.",
"invalidId": "El ID de la conexión PBS no es válido. Recarga la lista de conexiones.",
"invalidDatastores": "PBS ha devuelto una lista de datastores no válida.",
"connectionLimit": "Se pueden configurar como máximo {count} conexiones PBS.",
"username": "Introduce el usuario de PBS con el formato usuario@realm, por ejemplo root@pam.",
"password": "Introduce la contraseña del usuario de PBS.",
"loginRejected": "PBS ha rechazado el usuario o la contraseña.",
"invalidLogin": "PBS ha devuelto una respuesta de inicio de sesión no válida.",
"tokenCreateDenied": "Este usuario de PBS no puede crear tokens API.",
"invalidToken": "PBS ha devuelto un token API no válido.",
"auditRoleDenied": "Este usuario de PBS no puede conceder el rol Audit. Inicia sesión como root@pam u otro administrador.",
"notFound": "No se encontró la conexión PBS. Recarga la lista de conexiones.",
"readFailed": "No se pudieron leer los datos de PBS. Revisa su servicio y los permisos.",
"invalidList": "PBS ha devuelto una lista de datos no válida.",
"localUnavailable": "PBS no está disponible en este host.",
"authRequired": "Activa la autenticación local en Monitor antes de gestionar conexiones PBS remotas.",
"datastoreOffline": "El datastore está desconectado, en modo de mantenimiento o su dispositivo no está montado.",
"datastoreUnavailable": "El sistema de archivos del datastore no está disponible."
},
"taskTypes": {
"backup": "Backup",
"restore": "Restauración",
"verify": "Verificación",
"verifyGroup": "Verificación de grupo de backups",
"verifySnapshot": "Verificación de snapshot",
"prune": "Prune",
"garbageCollection": "Recolección de basura",
"sync": "Sincronización",
"updateCheck": "Comprobación de actualizaciones",
"terminal": "Terminal",
"createDatastore": "Creación de datastore",
"other": "Otra tarea ({type})"
},
"states": {
"ok": "OK",
"unknown": "Desconocido",
"warnings": "Advertencias: {count}",
"warning": "Advertencia",
"error": "Error"
},
"serviceStates": {
"active": "Activo",
"inactive": "Inactivo",
"failed": "Fallido",
"activating": "Iniciando",
"deactivating": "Deteniendo",
"unknown": "Desconocido"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} d"
},
"urlHelp": "Incluye :8007 para el puerto predeterminado de PBS, por ejemplo https://pbs.example:8007.",
"usernameHelp": "Usa usuario@realm, por ejemplo root@pam.",
"tokenIdHelp": "Usa usuario@realm!token, por ejemplo monitor@pbs!proxmenux.",
"tokenSecretHelp": "Introduce el secreto de este token API.",
"tokenSecretKeepHelp": "Déjalo vacío para conservar el secreto del token guardado.",
"removeFailed": "No se pudo eliminar la conexión PBS.",
"savedRefreshFailed": "La conexión PBS se ha guardado, pero no se pudo actualizar la lista de conexiones. Recarga la página antes de hacer otro cambio.",
"removedRefreshFailed": "La conexión PBS se ha eliminado, pero no se pudo actualizar la lista de conexiones. Recarga la página.",
"technicalDetails": "Detalles técnicos",
"datastoreType": "Datastore"
},
"_meta": "Community translation target. Copy keys from ../en/common.json and translate the values.",
"app": {
@@ -1306,7 +1384,9 @@
"update": "Actualizar",
"modify": "Modificar",
"recover": "Recuperar",
"registerRestored": "Registrar de nuevo",
"terminalTitleUpdate": "Actualizar aplicación OCI",
"terminalTitleRecoverPrevious": "Recuperar la instalación OCI anterior",
"terminalTitleModify": "Modificar aplicación OCI",
"terminalTitleRecover": "Recuperar aplicación OCI restaurada",
"terminalDescription": "Se ejecuta el mismo proceso que en el menú de ProxMenux: OCI manager Apps → Gestionar aplicaciones OCI instaladas.",
@@ -1835,7 +1915,128 @@
"ociNewImage": "Nueva imagen",
"ociImageCurrent": "Versión",
"refreshData": "Actualizar datos",
"refreshingData": "Actualizando…"
"refreshingData": "Actualizando…",
"confirmRemove": "¿Eliminar el registro de esta aplicación de la pestaña App? Esto no desinstala la aplicación.",
"packageExample": "por ejemplo, {package}",
"packageHelpBefore": "Se rellena automáticamente a partir del nombre. Compruébalo con",
"packageHelpAfter": "dentro del CT.",
"binaryHelpBefore": "Ruta absoluta. Búscala con",
"binaryHelpOr": "o",
"errors": {
"ociBusy": "hay una operación OCI en curso en este contenedor",
"dpkgMissing": "{package} no está instalado mediante dpkg",
"dpkgFailed": "dpkg-query falló",
"dpkgNoVersion": "ninguna versión coincide en la salida de dpkg",
"apkFailed": "apk info falló",
"apkNoVersion": "ninguna versión coincide en la salida de apk",
"fileRead": "no se pudo leer {path}",
"fileNoMatch": "file_regex no coincide con {path}",
"binaryFailed": "falló la invocación del binario",
"binaryNoVersion": "ninguna versión coincide en la salida del binario",
"pythonFailed": "python -c importlib.metadata falló",
"pythonNoVersion": "ninguna versión coincide en la salida de la distribución de Python",
"dockerImageResolve": "no se pudo resolver la imagen instalada del contenedor",
"dockerInspectFailed": "docker inspect falló",
"dockerLabelMissing": "el contenedor no tiene la etiqueta {label}",
"dockerLabelMutable": "la etiqueta de docker indica {tag} (tag mutable, no una versión)",
"dockerLabelNoVersion": "ninguna versión coincide en la salida de la etiqueta de docker",
"dockerExecFailed": "docker exec falló",
"dockerExecNoVersion": "ninguna versión coincide en la salida de docker exec",
"commandEmpty": "command_argv está vacío",
"commandFailed": "el comando falló",
"commandNoVersion": "ninguna versión coincide en la salida del comando",
"unsupportedMethod": "método no compatible: {method}",
"unknownUpstream": "upstream_type desconocido: {type}",
"githubNotFound": "{repo}: no encontrado",
"githubRejected": "github rechazó la solicitud (403)",
"githubError": "error de github {code}",
"noRelease": "no hay tag ni nombre de release en la respuesta",
"tagNoMatch": "tag_regex no coincide con '{tag}'",
"httpError": "error http {code}",
"invalidJson": "respuesta JSON no válida: {detail}",
"jsonPathUnresolved": "json_path '{path}' no se pudo resolver",
"jsonEmpty": "valor vacío en json_path",
"dockerNoTags": "ningún tag coincide (docker_hub)",
"dockerMovingOnly": "tag_regex solo coincide con tags móviles; usa el seguimiento por digest de la imagen Docker",
"dockerHubError": "error de docker hub {code}",
"dockerHubJson": "JSON de docker hub no válido: {detail}",
"dockerHubEmpty": "docker hub no devolvió ningún tag",
"payload": "el payload debe ser un objeto JSON",
"name": "name es obligatorio y debe tener 1-64 caracteres de letras/dígitos/espacios/._+-()/",
"method": "installed_via debe ser uno de: {values} o vacío",
"package": "package es obligatorio (letras/dígitos/._+@:/ hasta 127 caracteres)",
"filePath": "file_path es obligatorio y debe ser una ruta absoluta",
"fileRegex": "file_regex es obligatorio",
"fileRegexInvalid": "file_regex no es una regex válida: {detail}",
"binaryPath": "binary_path es obligatorio y debe ser una ruta absoluta",
"pythonPath": "python_path es obligatorio y debe ser una ruta absoluta",
"distribution": "distribution es obligatorio (nombre PEP 503)",
"container": "container_name es obligatorio (reglas de nombres de docker)",
"label": "label es obligatorio (reglas de nombres de etiquetas OCI)",
"dockerBinary": "binary_path es obligatorio (ruta absoluta o comando simple)",
"manualVersion": "installed_version es obligatorio para el método manual",
"versionLength": "installed_version supera {max} caracteres",
"versionControl": "installed_version contiene un carácter de control no permitido",
"installedRegexInvalid": "installed_regex no es una regex válida: {detail}",
"upstreamType": "upstream_type debe ser uno de: {values}",
"tagRegexInvalid": "tag_regex no es una regex válida: {detail}",
"repoRequired": "repo es obligatorio para upstream_type=github",
"repoFormat": "repo debe tener el formato 'owner/name'",
"githubSource": "github_source debe ser uno de: {values}",
"upstreamUrlRequired": "upstream_url es obligatorio para upstream_type=http_json",
"upstreamUrl": "upstream_url debe ser una URL http(s)",
"upstreamUrlLength": "upstream_url supera {max} caracteres",
"jsonPathRequired": "upstream_json_path es obligatorio para upstream_type=http_json",
"jsonPathLength": "upstream_json_path supera {max} caracteres",
"jsonPathCharacters": "upstream_json_path usa caracteres no permitidos",
"dockerImageRequired": "docker_image es obligatorio para upstream_type=docker_hub",
"dockerImageLength": "docker_image supera {max} caracteres",
"dockerImageFormat": "docker_image debe seguir el formato de nombres de Docker Hub (owner/name o name)",
"healthPath": "health_path debe ser una ruta URL válida (máx. 255 caracteres)",
"logoUrl": "logo_url debe ser una URL http(s) o el icono local en caché de esta VM",
"discoveryId": "discovery_id no válido",
"helperSlug": "helper_slug debe ser un slug en minúsculas (letras/dígitos/._-)",
"updateVia": "update_via solo acepta 'docker'",
"dockerDetector": "update_via=docker requiere un detector docker_label o docker_exec",
"dockerConflict": "update_via=docker no se puede combinar con {field}",
"updateCommandType": "update_command debe ser una cadena",
"updateCommandLength": "update_command supera {max} caracteres",
"updateCommandNull": "update_command contiene un byte nulo",
"updateMethod": "update_method debe ser none, helper o custom",
"updateCommandRequired": "update_command es obligatorio para update_method=custom",
"updateCommandOnlyCustom": "update_command solo se permite con update_method=custom",
"helperRequired": "se requiere un helper_slug compatible para update_method=helper",
"dockerUpdater": "las aplicaciones delegadas en Docker se actualizan mediante su imagen, no con un actualizador de aplicación aparte",
"commandRequired": "command_argv es obligatorio (lista no vacía)",
"commandList": "command_argv debe ser una lista no vacía de cadenas",
"commandCount": "command_argv admite como máximo {max} entradas",
"commandEntry": "command_argv[{index}] debe ser una cadena no vacía",
"commandEntryLength": "command_argv[{index}] supera {max} caracteres",
"commandEntryControl": "command_argv[{index}] contiene un carácter de control no permitido",
"binaryArgsList": "binary_args debe ser una lista de cadenas",
"binaryArgsCount": "binary_args admite como máximo {max} entradas",
"binaryArgEntry": "binary_args[{index}] debe ser una cadena no vacía",
"binaryArgLength": "binary_args[{index}] supera {max} caracteres",
"binaryArgControl": "binary_args[{index}] contiene un carácter de control no permitido",
"portsList": "ports debe ser una lista de {port, description, scheme}",
"portObject": "ports[{index}] debe ser un objeto",
"portRequired": "ports[{index}].port es obligatorio",
"portInteger": "ports[{index}].port debe ser un número entero",
"portRange": "ports[{index}].port debe estar en el rango 1-65535",
"portDuplicate": "el puerto {port} aparece más de una vez en esta aplicación",
"portDescription": "ports[{index}].description tiene caracteres no válidos",
"portScheme": "ports[{index}].scheme debe ser 'http' o 'https'",
"portPath": "ports[{index}].web_path debe ser una ruta URL válida (máx. 255 caracteres)",
"portLogo": "ports[{index}].logo_url debe ser una URL http(s) (máx. 512 caracteres)",
"portCategory": "ports[{index}].category tiene caracteres no válidos o es demasiado largo",
"portCustomUrl": "ports[{index}].custom_url debe ser una URL http(s) (máx. 512 caracteres)",
"tagRegexLength": "tag_regex supera 512 caracteres",
"persistPermission": "no se pudo guardar el sidecar (¿permisos?)",
"persist": "no se pudo guardar el sidecar",
"noApps": "no hay aplicaciones registradas para este vmid",
"appMissing": "no se encontró app_id '{appId}'",
"appNotFound": "aplicación no encontrada"
}
},
"statusFilter": {
"ariaLabel": "Filtrar máquinas virtuales y contenedores",
@@ -2369,6 +2570,42 @@
"updateManyDescription": "Volver a ejecutar {count} funciones posteriores a la instalación en secuencia.",
"legacySourceTitle": "Optimización heredada: haz clic para ver la fuente",
"sourceTitle": "Haz clic para ver el código fuente",
"sourceDialogTitle": "Código fuente: {name}",
"sourceLoading": "Cargando el código fuente…",
"sourceUnavailable": "El código fuente no está disponible.",
"sourceLoadFailed": "No se pudo cargar el código fuente. Vuelve a intentarlo.",
"sourceFunctionMissing": "No se encontró la función en los scripts disponibles.",
"sourceUnknownTool": "Herramienta desconocida: {tool}",
"sourceNoMapping": "No hay ninguna función asignada a la herramienta {tool}.",
"sourceToolNames": {
"subscription_banner": "Eliminación del banner de suscripción",
"time_sync": "Sincronización horaria",
"apt_languages": "Omitir idiomas de APT",
"journald": "Optimización de Journald",
"logrotate": "Optimización de Logrotate",
"system_limits": "Aumento de los límites del sistema",
"memory_settings": "Optimización de la configuración de memoria",
"zram_swap": "Perfil de swap con ZRAM",
"kernel_panic": "Configuración de kernel panic",
"apt_ipv4": "Forzar IPv4 en APT",
"kexec": "kexec para reinicios rápidos",
"rpc": "Desactivar RPC / rpcbind",
"motd": "Banner MOTD personalizado",
"system_utils": "Utilidades del sistema",
"network_optimization": "Optimizaciones de red",
"bashrc_custom": "Personalización de Bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (protección de SSD)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Correcciones para CPU AMD (Ryzen/EPYC)",
"persistent_network": "Configurar interfaces de red persistentes",
"vfio_iommu": "Passthrough VFIO/IOMMU",
"lvm_repair": "Reparación de cabeceras PV de LVM",
"repo_cleanup": "Limpieza de repositorios",
"proxmox_repos": "Repositorios APT de Proxmox",
"entropy": "Generación de entropía (haveged)"
},
"legacy": "legado",
"updateToolTitle": "Actualizar {name} a v{version}",
"availableUpdates": "Actualizaciones disponibles",
@@ -4895,6 +5132,124 @@
"effect": "Se aplica a los próximos backups que hereden la política de este almacenamiento. Guardar no elimina los backups existentes, y los protegidos siguen protegidos.",
"cancel": "Cancelar",
"save": "Guardar retención"
},
"apiErrors": {
"requestFailed": "La solicitud ha fallado.",
"absolutePath": "la ruta debe ser absoluta",
"pathConfigured": "esta ruta ya está configurada",
"pathRequired": "la ruta es obligatoria",
"pathQuery": "el parámetro de consulta path es obligatorio",
"pathNotFound": "ruta no encontrada",
"invalidJobId": "id de tarea no válido",
"jobNotFound": "tarea no encontrada",
"runnerMissing": "el script de ejecución no está instalado",
"noRestore": "no hay ningún estado de restauración que descartar",
"invalidFile": "nombre de archivo no válido",
"notFound": "no encontrado",
"logOutside": "la ruta del log está fuera del directorio permitido",
"targetsList": "targets debe ser una lista",
"emptyExpression": "expresión vacía",
"invalidId": "id no válido (usa letras, dígitos, _ o -)",
"backend": "backend debe ser pbs, local o borg",
"remoteBackend": "backend debe ser pbs o borg",
"remoteBackendQuoted": "backend debe ser 'pbs' o 'borg'",
"profileMode": "profile_mode debe ser default o custom",
"borgAttached": "borg no se puede vincular (vzdump de PVE solo escribe en almacenamientos pbs/local)",
"borgAttachedLong": "borg no es un backend válido para tareas vinculadas (vzdump de PVE solo escribe en almacenamientos pbs/local)",
"parentRequired": "pve_parent_job_id y pve_storage son obligatorios para attached",
"absolutePaths": "paths debe ser una lista de rutas absolutas",
"customPath": "el perfil personalizado necesita al menos una ruta",
"defaultPaths": "no se pudieron resolver las rutas del perfil predeterminado",
"retentionObject": "retention debe ser un objeto",
"pbsRepository": "pbs_repository es obligatorio",
"pbsEncryption": "pbs_encrypt_mode debe ser 'none', 'new' o 'existing'",
"pbsKeyFailed": "no se pudo crear el archivo de claves de cifrado de PBS",
"pbsClientMissing": "no se pudo crear el archivo de claves de cifrado de PBS (¿está instalado proxmox-backup-client?)",
"borgRepository": "borg_repo es obligatorio",
"borgEncryption": "borg_encrypt_mode debe ser uno de: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode debe ser uno de: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase es obligatorio cuando el cifrado está activado (guárdala en el destino o indica una en la solicitud)",
"passphrase": "la frase de contraseña es obligatoria cuando el cifrado está activado",
"password": "la contraseña es obligatoria",
"deviceConfirmation": "confirm_device debe coincidir exactamente con device (comprobación de errores tipográficos / seguridad)",
"invalidDevice": "ruta de dispositivo no válida",
"absoluteMountpoint": "se requiere un punto de montaje absoluto",
"archiveLayout": "estructura del archivo no reconocida: no se encontró rootfs/",
"archiveMissing": "archivo no encontrado",
"archiveNotFile": "la ruta del archivo no es un archivo regular",
"invalidName": "nombre no válido",
"invalidStaging": "staging_path no válido",
"jobRename": "no se puede cambiar el nombre de la tarea (elimínala y vuelve a crearla para renombrarla)",
"absoluteKey": "key_path debe ser absoluta",
"mode": "mode debe ser \"local\" o \"ssh\"",
"nameRequired": "el nombre es obligatorio (letras, dígitos, _ o -)",
"noLog": "aún no hay ningún log disponible para esta tarea",
"noPbsConfig": "no hay archivo de configuraciones manuales de PBS",
"outputMissing": "falta el archivo de salida",
"parserJson": "la salida del analizador no era un JSON válido",
"restoreJson": "la salida de run_restore no era un JSON válido",
"repoRequired": "repo es obligatorio",
"repoSnapshot": "repo_name y snapshot son obligatorios",
"borgRepoSnapshot": "repo_name y snapshot son obligatorios para borg",
"pbsRepoSnapshot": "repo_name y snapshot son obligatorios para pbs",
"repoName": "repo_name es obligatorio",
"restoreMissing": "las herramientas de restauración no están instaladas en este host",
"serverDatastore": "el servidor y el datastore son obligatorios",
"snapshotRequired": "el snapshot es obligatorio",
"source": "source debe ser pbs, borg o local",
"sshPort": "ssh_port debe ser un número entero entre 1 y 65535",
"sshRequired": "ssh_user, ssh_host y ssh_remote_path son obligatorios en el modo ssh",
"repositoryPath": "el destino no tiene ruta de repositorio",
"taskExpired": "tarea no encontrada o caducada",
"invalidExpression": "expresión no válida",
"noReport": "no se ha emitido ningún informe",
"parseManifestFailed": "parse_manifest terminó con un código distinto de cero",
"jobExists": "la tarea ya existe: {value}",
"pathMissing": "la ruta no existe en este host: {value}",
"deviceMissing": "el dispositivo no existe: {value}",
"mountpoint": "no es un punto de montaje: {value}",
"localArchiveMissing": "archivo local no encontrado: {value}",
"borgDestination": "no se encontró el destino Borg '{value}'",
"pbsDestination": "no se encontró el destino PBS '{value}'",
"pbsPassword": "no hay contraseña disponible para el repo PBS '{value}'",
"borgRepoMissing": "el repo Borg \"{value}\" no está configurado",
"pbsRepoMissing": "el repo PBS \"{value}\" no está configurado",
"borgTargetMissing": "destino Borg no encontrado: {value}",
"pbsTargetMissing": "repo PBS no encontrado: {value}",
"pveJobMissing": "tarea vzdump de PVE no encontrada: {value}",
"statePersist": "no se puede guardar el estado: {value}",
"sshDirectory": "no se puede preparar el directorio ssh: {value}",
"logRead": "no se puede leer el log: {value}",
"envUpdate": "no se pudo actualizar .env: {value}",
"pbsConfigWrite": "no se pudo guardar la configuración de PBS: {value}",
"persist": "no se pudo guardar: {value}",
"archiveRemove": "no se pudo eliminar el archivo: {value}",
"start": "no se pudo iniciar: {value}",
"borgTargetsUpdate": "no se pudo actualizar borg-targets.txt: {value}",
"pbsTargetsUpdate": "no se pudo actualizar pbs-manual-configs.txt: {value}",
"unitsUpdate": "no se pudieron actualizar las unidades de systemd: {value}",
"borgTargetsWrite": "no se pudo escribir borg-targets.txt: {value}",
"jobWrite": "no se pudieron escribir los archivos de la tarea: {value}",
"localTargetWrite": "no se pudo escribir local-target.conf: {value}",
"unitsWrite": "no se pudieron escribir las unidades de systemd: {value}",
"format": "falló el formateo: {value}",
"calendar": "on_calendar no válido: {value}",
"mount": "falló el montaje: {value}",
"parserInvocation": "falló la invocación del analizador: {value}",
"preflightInvocation": "falló la invocación de la verificación previa: {value}",
"publicKeyMissing": "no se encontró la clave pública tras generarla: {value}",
"retentionInteger": "retention.{value} debe ser un número entero",
"retentionNegative": "retention.{value} no puede ser negativo",
"keygen": "ssh-keygen falló: {value}",
"systemctl": "systemctl falló: {value}",
"unmount": "umount falló: {value}",
"taskIncomplete": "la tarea está en estado \"{value}\", no completada",
"unknownKind": "tipo desconocido: {value}",
"unknownMode": "modo desconocido \"{value}\"",
"defaultRemove": "{value} es el valor predeterminado integrado y no se puede eliminar",
"defaultAdd": "{value} es el valor predeterminado integrado; no hace falta añadirlo",
"keyImport": "No hay ningún archivo de claves de PBS instalado en {value}. Importa primero el archivo de claves.",
"storageMismatch": "el almacenamiento de la tarea de PVE no coincide (la tarea usa {value}, el payload indica {other})"
}
},
"healthStatus": {
@@ -5396,6 +5751,7 @@
"websocketError": "Ocurrió un error en WebSocket",
"connectionClosed": "Conexión cerrada",
"completedSuccessfully": "Guión completado con éxito",
"cancelled": "Operación cancelada.",
"completedWithError": "Script terminado con el código de error {code}",
"yes": "Sí",
"yourInput": "Su entrada:",
@@ -7408,6 +7764,17 @@
"addSelected": "Añadir seleccionados",
"pbsWebInterface": "Interfaz web de PBS",
"changeAddress": "Cambiar dirección",
"cancel": "Cancelar"
"cancel": "Cancelar",
"errors": {
"addressRequired": "Introduce la dirección del Monitor.",
"invalidAddress": "La dirección debe empezar por http:// o https:// e indicar un host.",
"addressCredentials": "La dirección no debe contener un usuario ni una contraseña.",
"invalidPort": "El puerto de la dirección no es válido.",
"invalidRequest": "La solicitud no es válida. Recarga la página y vuelve a intentarlo.",
"limit": "Se pueden añadir como máximo {count} nodos.",
"duplicate": "Esta dirección ya está en la lista.",
"notFound": "No se encontró el nodo. Recarga la lista y vuelve a intentarlo.",
"detectedRemove": "Solo se pueden eliminar los nodos añadidos a mano. Los nodos detectados se pueden ocultar."
}
}
}
+375 -8
View File
@@ -1231,7 +1231,9 @@
"update": "Mettre à jour",
"modify": "Modifier",
"recover": "Récupérer",
"registerRestored": "Enregistrer à nouveau",
"terminalTitleUpdate": "Mettre à jour l'application OCI",
"terminalTitleRecoverPrevious": "Récupérer l'installation OCI précédente",
"terminalTitleModify": "Modifier l'application OCI",
"terminalTitleRecover": "Récupérer l'application OCI restaurée",
"terminalDescription": "Le même processus que dans le menu ProxMenux s'exécute : OCI manager Apps → Gérer les applications OCI installées.",
@@ -1760,7 +1762,128 @@
"dockerEvidenceImage": "Image",
"dockerEvidencePort": "Port TCP publié",
"dockerEvidenceProject": "Projet Compose",
"dockerEndpointReview": "Conteneur avec des ports TCP publiés. Confirmez le nom et l'adresse web avant d'enregistrer. L'enregistrement ne configure pas les mises à jour."
"dockerEndpointReview": "Conteneur avec des ports TCP publiés. Confirmez le nom et l'adresse web avant d'enregistrer. L'enregistrement ne configure pas les mises à jour.",
"confirmRemove": "Supprimer l'enregistrement de cette application de l'onglet App ? Cela ne désinstalle pas l'application.",
"packageExample": "par ex. {package}",
"packageHelpBefore": "Rempli automatiquement à partir du nom. Vérifiez avec",
"packageHelpAfter": "dans le CT.",
"binaryHelpBefore": "Chemin absolu. Trouvez-le avec",
"binaryHelpOr": "ou",
"errors": {
"ociBusy": "une opération OCI est en cours sur ce conteneur",
"dpkgMissing": "{package} n'est pas installé via dpkg",
"dpkgFailed": "échec de dpkg-query",
"dpkgNoVersion": "aucune version trouvée dans la sortie de dpkg",
"apkFailed": "échec de apk info",
"apkNoVersion": "aucune version trouvée dans la sortie de apk",
"fileRead": "impossible de lire {path}",
"fileNoMatch": "file_regex ne correspond pas à {path}",
"binaryFailed": "échec de l'appel du binaire",
"binaryNoVersion": "aucune version trouvée dans la sortie du binaire",
"pythonFailed": "échec de python -c importlib.metadata",
"pythonNoVersion": "aucune version trouvée dans la sortie de la distribution Python",
"dockerImageResolve": "impossible de déterminer l'image installée du conteneur",
"dockerInspectFailed": "échec de docker inspect",
"dockerLabelMissing": "le conteneur n'a pas d'étiquette {label}",
"dockerLabelMutable": "l'étiquette docker indique {tag} (tag mobile, pas une version)",
"dockerLabelNoVersion": "aucune version trouvée dans la sortie de l'étiquette docker",
"dockerExecFailed": "échec de docker exec",
"dockerExecNoVersion": "aucune version trouvée dans la sortie de docker exec",
"commandEmpty": "command_argv est vide",
"commandFailed": "échec de la commande",
"commandNoVersion": "aucune version trouvée dans la sortie de la commande",
"unsupportedMethod": "méthode non prise en charge : {method}",
"unknownUpstream": "upstream_type inconnu : {type}",
"githubNotFound": "{repo} : introuvable",
"githubRejected": "github a refusé la requête (403)",
"githubError": "erreur github {code}",
"noRelease": "aucun tag ni nom de release dans la réponse",
"tagNoMatch": "tag_regex ne correspond pas à '{tag}'",
"httpError": "erreur http {code}",
"invalidJson": "réponse JSON non valide : {detail}",
"jsonPathUnresolved": "json_path '{path}' n'a pas pu être résolu",
"jsonEmpty": "valeur vide à json_path",
"dockerNoTags": "aucun tag ne correspond (docker_hub)",
"dockerMovingOnly": "tag_regex ne correspond qu'à des tags mobiles ; utilisez le suivi par digest de l'image Docker",
"dockerHubError": "erreur docker hub {code}",
"dockerHubJson": "JSON docker hub non valide : {detail}",
"dockerHubEmpty": "aucun tag renvoyé par docker hub",
"payload": "la charge utile doit être un objet JSON",
"name": "name est obligatoire et doit comporter de 1 à 64 caractères parmi lettres/chiffres/espaces/._+-()/",
"method": "installed_via doit être l'une des valeurs suivantes : {values} ou vide",
"package": "package est obligatoire (lettres/chiffres/._+@:/, 127 caractères max.)",
"filePath": "file_path est obligatoire et doit être un chemin absolu",
"fileRegex": "file_regex est obligatoire",
"fileRegexInvalid": "file_regex n'est pas une regex valide : {detail}",
"binaryPath": "binary_path est obligatoire et doit être un chemin absolu",
"pythonPath": "python_path est obligatoire et doit être un chemin absolu",
"distribution": "distribution est obligatoire (nom PEP 503)",
"container": "container_name est obligatoire (règles de nommage docker)",
"label": "label est obligatoire (règles de nommage des étiquettes OCI)",
"dockerBinary": "binary_path est obligatoire (chemin absolu ou commande seule)",
"manualVersion": "installed_version est obligatoire pour la méthode manuelle",
"versionLength": "installed_version dépasse {max} caractères",
"versionControl": "installed_version contient un caractère de contrôle interdit",
"installedRegexInvalid": "installed_regex n'est pas une regex valide : {detail}",
"upstreamType": "upstream_type doit être l'une des valeurs suivantes : {values}",
"tagRegexInvalid": "tag_regex n'est pas une regex valide : {detail}",
"repoRequired": "repo est obligatoire pour upstream_type=github",
"repoFormat": "repo doit être au format 'owner/name'",
"githubSource": "github_source doit être l'une des valeurs suivantes : {values}",
"upstreamUrlRequired": "upstream_url est obligatoire pour upstream_type=http_json",
"upstreamUrl": "upstream_url doit être une URL http(s)",
"upstreamUrlLength": "upstream_url dépasse {max} caractères",
"jsonPathRequired": "upstream_json_path est obligatoire pour upstream_type=http_json",
"jsonPathLength": "upstream_json_path dépasse {max} caractères",
"jsonPathCharacters": "upstream_json_path utilise des caractères interdits",
"dockerImageRequired": "docker_image est obligatoire pour upstream_type=docker_hub",
"dockerImageLength": "docker_image dépasse {max} caractères",
"dockerImageFormat": "docker_image doit respecter le nommage Docker Hub (owner/name ou name)",
"healthPath": "health_path doit être un chemin d'URL valide (255 caractères max.)",
"logoUrl": "logo_url doit être une URL http(s) ou l'icône locale en cache de cette VM",
"discoveryId": "discovery_id non valide",
"helperSlug": "helper_slug doit être un slug en minuscules (lettres/chiffres/._-)",
"updateVia": "update_via n'accepte que 'docker'",
"dockerDetector": "update_via=docker nécessite un détecteur docker_label ou docker_exec",
"dockerConflict": "update_via=docker ne peut pas être combiné avec {field}",
"updateCommandType": "update_command doit être une chaîne",
"updateCommandLength": "update_command dépasse {max} caractères",
"updateCommandNull": "update_command contient un octet nul",
"updateMethod": "update_method doit être none, helper ou custom",
"updateCommandRequired": "update_command est obligatoire pour update_method=custom",
"updateCommandOnlyCustom": "update_command n'est autorisé que pour update_method=custom",
"helperRequired": "un helper_slug pris en charge est obligatoire pour update_method=helper",
"dockerUpdater": "Les applications déléguées à Docker se mettent à jour via leur image, pas via un outil de mise à jour d'application distinct",
"commandRequired": "command_argv est obligatoire (liste non vide)",
"commandList": "command_argv doit être une liste non vide de chaînes",
"commandCount": "command_argv accepte au maximum {max} entrées",
"commandEntry": "command_argv[{index}] doit être une chaîne non vide",
"commandEntryLength": "command_argv[{index}] dépasse {max} caractères",
"commandEntryControl": "command_argv[{index}] contient un caractère de contrôle interdit",
"binaryArgsList": "binary_args doit être une liste de chaînes",
"binaryArgsCount": "binary_args accepte au maximum {max} entrées",
"binaryArgEntry": "binary_args[{index}] doit être une chaîne non vide",
"binaryArgLength": "binary_args[{index}] dépasse {max} caractères",
"binaryArgControl": "binary_args[{index}] contient un caractère de contrôle interdit",
"portsList": "ports doit être une liste de {port, description, scheme}",
"portObject": "ports[{index}] doit être un objet",
"portRequired": "ports[{index}].port est obligatoire",
"portInteger": "ports[{index}].port doit être un entier",
"portRange": "ports[{index}].port doit être compris entre 1 et 65535",
"portDuplicate": "le port {port} apparaît plusieurs fois pour cette application",
"portDescription": "ports[{index}].description contient des caractères non valides",
"portScheme": "ports[{index}].scheme doit être 'http' ou 'https'",
"portPath": "ports[{index}].web_path doit être un chemin d'URL valide (255 caractères max.)",
"portLogo": "ports[{index}].logo_url doit être une URL http(s) (512 caractères max.)",
"portCategory": "ports[{index}].category contient des caractères non valides ou est trop long",
"portCustomUrl": "ports[{index}].custom_url doit être une URL http(s) (512 caractères max.)",
"tagRegexLength": "tag_regex dépasse 512 caractères",
"persistPermission": "impossible d'enregistrer le fichier sidecar (problème de permissions ?)",
"persist": "impossible d'enregistrer le fichier sidecar",
"noApps": "aucune application enregistrée pour ce vmid",
"appMissing": "app_id '{appId}' introuvable",
"appNotFound": "application introuvable"
}
},
"statusFilter": {
"ariaLabel": "Filtrer les machines virtuelles et les conteneurs",
@@ -2294,6 +2417,42 @@
"updateManyDescription": "Réexécution des fonctions de post-installation {count} dans l'ordre.",
"legacySourceTitle": "Optimisation héritée – cliquez pour afficher la source",
"sourceTitle": "Cliquez pour voir le code source",
"sourceDialogTitle": "Code source : {name}",
"sourceLoading": "Chargement du code source…",
"sourceUnavailable": "Le code source n'est pas disponible.",
"sourceLoadFailed": "Impossible de charger le code source. Réessayez.",
"sourceFunctionMissing": "La fonction est introuvable dans les scripts disponibles.",
"sourceUnknownTool": "Outil inconnu : {tool}",
"sourceNoMapping": "Aucune fonction n'est associée à l'outil {tool}.",
"sourceToolNames": {
"subscription_banner": "Suppression de la bannière d'abonnement",
"time_sync": "Synchronisation de l'heure",
"apt_languages": "Ignorer les langues APT",
"journald": "Optimisation de journald",
"logrotate": "Optimisation de logrotate",
"system_limits": "Augmentation des limites du système",
"memory_settings": "Optimisation des paramètres de mémoire",
"zram_swap": "Profil d'échange ZRAM",
"kernel_panic": "Configuration de la panique du noyau",
"apt_ipv4": "Forcer IPv4 pour APT",
"kexec": "kexec pour des redémarrages rapides",
"rpc": "Désactivation de RPC / rpcbind",
"motd": "Bannière MOTD personnalisée",
"system_utils": "Utilitaires système",
"network_optimization": "Optimisations du réseau",
"bashrc_custom": "Personnalisation de Bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (protection SSD)",
"zfs_autotrim": "Autotrim ZFS",
"amd_fixes": "Correctifs CPU AMD (Ryzen/EPYC)",
"persistent_network": "Configuration d'interfaces réseau persistantes",
"vfio_iommu": "Passthrough VFIO/IOMMU",
"lvm_repair": "Réparation des en-têtes PV LVM",
"repo_cleanup": "Nettoyage des référentiels",
"proxmox_repos": "Référentiels APT Proxmox",
"entropy": "Génération d'entropie (haveged)"
},
"legacy": "héritage",
"updateToolTitle": "Mettez à jour {name} vers v{version}",
"availableUpdates": "Mises à jour disponibles",
@@ -4820,6 +4979,124 @@
"cancel": "Annuler",
"save": "Enregistrer la rétention",
"useDefault": "Utiliser la valeur par défaut de Proxmox (aucune politique sur ce stockage)"
},
"apiErrors": {
"requestFailed": "La requête a échoué.",
"absolutePath": "le chemin doit être absolu",
"pathConfigured": "ce chemin est déjà configuré",
"pathRequired": "le chemin est obligatoire",
"pathQuery": "le paramètre de requête path est obligatoire",
"pathNotFound": "chemin introuvable",
"invalidJobId": "ID de tâche non valide",
"jobNotFound": "tâche introuvable",
"runnerMissing": "script d'exécution non installé",
"noRestore": "aucun état de restauration à ignorer",
"invalidFile": "nom de fichier non valide",
"notFound": "introuvable",
"logOutside": "chemin du journal hors du répertoire autorisé",
"targetsList": "targets doit être une liste",
"emptyExpression": "expression vide",
"invalidId": "ID non valide (utilisez des lettres, des chiffres, _ ou -)",
"backend": "le backend doit être pbs, local ou borg",
"remoteBackend": "le backend doit être pbs ou borg",
"remoteBackendQuoted": "le backend doit être 'pbs' ou 'borg'",
"profileMode": "profile_mode doit être default ou custom",
"borgAttached": "borg ne peut pas être rattaché (vzdump de PVE n'écrit que sur des stockages pbs/local)",
"borgAttachedLong": "borg n'est pas un backend valide pour les tâches rattachées (vzdump de PVE n'écrit que sur des stockages pbs/local)",
"parentRequired": "pve_parent_job_id et pve_storage sont obligatoires pour une tâche rattachée",
"absolutePaths": "paths doit être une liste de chemins absolus",
"customPath": "le profil personnalisé nécessite au moins un chemin",
"defaultPaths": "impossible de déterminer les chemins du profil par défaut",
"retentionObject": "retention doit être un objet",
"pbsRepository": "pbs_repository est obligatoire",
"pbsEncryption": "pbs_encrypt_mode doit être 'none', 'new' ou 'existing'",
"pbsKeyFailed": "échec de la création du fichier de clé de chiffrement PBS",
"pbsClientMissing": "échec de la création du fichier de clé de chiffrement PBS (proxmox-backup-client est-il installé ?)",
"borgRepository": "borg_repo est obligatoire",
"borgEncryption": "borg_encrypt_mode doit être l'une des valeurs suivantes : none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode doit être l'une des valeurs suivantes : none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase est obligatoire lorsque le chiffrement est activé (enregistrez-la sur la destination ou transmettez-en une dans la requête)",
"passphrase": "la phrase secrète est obligatoire lorsque le chiffrement est activé",
"password": "le mot de passe est obligatoire",
"deviceConfirmation": "confirm_device doit correspondre exactement au périphérique (contrôle de frappe / sécurité)",
"invalidDevice": "chemin de périphérique non valide",
"absoluteMountpoint": "point de montage absolu requis",
"archiveLayout": "structure d'archive non reconnue — aucun rootfs/ trouvé",
"archiveMissing": "archive introuvable",
"archiveNotFile": "le chemin de l'archive n'est pas un fichier ordinaire",
"invalidName": "nom non valide",
"invalidStaging": "staging_path non valide",
"jobRename": "le nom de la tâche ne peut pas être modifié (supprimez-la puis recréez-la pour la renommer)",
"absoluteKey": "key_path doit être absolu",
"mode": "mode doit être \"local\" ou \"ssh\"",
"nameRequired": "le nom est obligatoire (lettres, chiffres, _ ou -)",
"noLog": "aucun journal disponible pour cette tâche pour le moment",
"noPbsConfig": "aucun fichier de configurations PBS manuelles",
"outputMissing": "fichier de sortie manquant",
"parserJson": "la sortie de l'analyseur n'était pas un JSON valide",
"restoreJson": "la sortie de run_restore n'était pas un JSON valide",
"repoRequired": "repo est obligatoire",
"repoSnapshot": "repo_name et snapshot sont obligatoires",
"borgRepoSnapshot": "repo_name et snapshot sont obligatoires pour borg",
"pbsRepoSnapshot": "repo_name et snapshot sont obligatoires pour pbs",
"repoName": "repo_name est obligatoire",
"restoreMissing": "outils de restauration non installés sur cet hôte",
"serverDatastore": "server et datastore sont obligatoires",
"snapshotRequired": "snapshot est obligatoire",
"source": "source doit être pbs, borg ou local",
"sshPort": "ssh_port doit être un entier compris entre 1 et 65535",
"sshRequired": "ssh_user, ssh_host et ssh_remote_path sont obligatoires pour le mode ssh",
"repositoryPath": "la cible n'a pas de chemin de référentiel",
"taskExpired": "tâche introuvable ou expirée",
"invalidExpression": "expression non valide",
"noReport": "aucun rapport émis",
"parseManifestFailed": "parse_manifest s'est terminé avec un code non nul",
"jobExists": "la tâche existe déjà : {value}",
"pathMissing": "le chemin n'existe pas sur cet hôte : {value}",
"deviceMissing": "le périphérique n'existe pas : {value}",
"mountpoint": "pas un point de montage : {value}",
"localArchiveMissing": "archive locale introuvable : {value}",
"borgDestination": "Destination Borg '{value}' introuvable",
"pbsDestination": "Destination PBS '{value}' introuvable",
"pbsPassword": "aucun mot de passe disponible pour le référentiel PBS '{value}'",
"borgRepoMissing": "Référentiel Borg \"{value}\" non configuré",
"pbsRepoMissing": "Référentiel PBS \"{value}\" non configuré",
"borgTargetMissing": "Cible Borg introuvable : {value}",
"pbsTargetMissing": "Référentiel PBS introuvable : {value}",
"pveJobMissing": "Tâche vzdump PVE introuvable : {value}",
"statePersist": "impossible d'enregistrer l'état : {value}",
"sshDirectory": "impossible de préparer le répertoire ssh : {value}",
"logRead": "impossible de lire le journal : {value}",
"envUpdate": "impossible de mettre à jour .env : {value}",
"pbsConfigWrite": "échec de l'enregistrement de la configuration PBS : {value}",
"persist": "échec de l'enregistrement : {value}",
"archiveRemove": "échec de la suppression de l'archive : {value}",
"start": "échec du démarrage : {value}",
"borgTargetsUpdate": "échec de la mise à jour de borg-targets.txt : {value}",
"pbsTargetsUpdate": "échec de la mise à jour de pbs-manual-configs.txt : {value}",
"unitsUpdate": "échec de la mise à jour des unités systemd : {value}",
"borgTargetsWrite": "échec de l'écriture de borg-targets.txt : {value}",
"jobWrite": "échec de l'écriture des fichiers de la tâche : {value}",
"localTargetWrite": "échec de l'écriture de local-target.conf : {value}",
"unitsWrite": "échec de l'écriture des unités systemd : {value}",
"format": "échec du formatage : {value}",
"calendar": "on_calendar non valide : {value}",
"mount": "échec du montage : {value}",
"parserInvocation": "échec de l'appel de l'analyseur : {value}",
"preflightInvocation": "échec de l'appel de la vérification préalable : {value}",
"publicKeyMissing": "clé publique introuvable après la génération : {value}",
"retentionInteger": "retention.{value} doit être un entier",
"retentionNegative": "retention.{value} doit être positif ou nul",
"keygen": "échec de ssh-keygen : {value}",
"systemctl": "échec de systemctl : {value}",
"unmount": "échec de umount : {value}",
"taskIncomplete": "la tâche est dans l'état \"{value}\", et non terminée",
"unknownKind": "type inconnu : {value}",
"unknownMode": "mode inconnu \"{value}\"",
"defaultRemove": "{value} est la valeur par défaut intégrée et ne peut pas être supprimé",
"defaultAdd": "{value} est la valeur par défaut intégrée ; inutile de l'ajouter",
"keyImport": "Aucun fichier de clé PBS n'est installé dans {value}. Importez d'abord le fichier de clé.",
"storageMismatch": "Incohérence du stockage de la tâche PVE (la tâche utilise {value}, la requête indique {other})"
}
},
"healthStatus": {
@@ -5320,6 +5597,7 @@
"websocketError": "Une erreur WebSocket s'est produite",
"connectionClosed": "Connexion fermée",
"completedSuccessfully": "Script terminé avec succès",
"cancelled": "Opération annulée.",
"completedWithError": "Script terminé avec le code d'erreur {code}",
"yes": "Oui",
"yourInput": "Votre contribution :",
@@ -7311,7 +7589,7 @@
"signInWith": "Se connecter avec",
"methodPassword": "Utilisateur et mot de passe",
"methodToken": "Jeton API",
"username": "Utilisateur (utilisateur@realm)",
"username": "Utilisateur",
"password": "Mot de passe",
"passwordHelp": "Le mot de passe n'est utilisé qu'une fois pour créer un jeton API en lecture seule (rôle Audit) pour ce Monitor. Seul ce jeton est enregistré, jamais le mot de passe. Activez d'abord l'authentification du Monitor.",
"cancel": "Annuler",
@@ -7324,10 +7602,10 @@
"editConnection": "Connexion à modifier",
"add": "Ajouter une connexion",
"name": "Nom",
"url": "URL HTTPS (inclure :8007 pour le port natif de PBS)",
"token_id": "Identifiant du jeton API (user@realm!token)",
"token_secret": "Secret du jeton API (laisser vide pour conserver le secret enregistré)",
"fingerprint": "Empreinte SHA256 du certificat (facultative)",
"url": "Adresse HTTPS",
"token_id": "ID du jeton API",
"token_secret": "Secret du jeton API",
"fingerprint": "Empreinte SHA256 (facultative)",
"tlsHelp": "Laissez l'empreinte vide pour la vérification habituelle de l'autorité de certification et du nom d'hôte. Pour un certificat autosigné, copiez son empreinte SHA256 depuis la console PBS ou un autre canal de confiance. L'empreinte est vérifiée avant l'envoi des identifiants.",
"saved": "Connexion PBS vérifiée et enregistrée.",
"saveFailed": "Impossible d'enregistrer la connexion PBS.",
@@ -7369,7 +7647,85 @@
"estimatedFull": "Estimé complet",
"estimatedFullUnknown": "Pas encore assez de données",
"duration": "Durée",
"noJobs": "Aucune planification de synchronisation, de vérification ou d'élagage n'est configurée."
"noJobs": "Aucune planification de synchronisation, de vérification ou d'élagage n'est configurée.",
"errors": {
"configRead": "Impossible de lire les connexions PBS enregistrées. Vérifiez le fichier de configuration et les permissions.",
"invalidConfig": "Les paramètres de connexion PBS ne sont pas valides. Vérifiez le formulaire et réessayez.",
"invalidField": "Un champ de la connexion n'est pas valide ou est trop long.",
"invalidUrl": "Utilisez une adresse PBS HTTPS sans identifiants intégrés, paramètres de requête ni fragment.",
"nameRequired": "Saisissez un nom de serveur de 128 caractères au maximum.",
"tokenIdFormat": "Saisissez l'ID du jeton API PBS au format user@realm!token.",
"tokenSecret": "Saisissez un secret de jeton API valide, sans espaces ni caractères de contrôle.",
"invalidTokenId": "L'ID du jeton API ne doit contenir que des caractères ASCII.",
"invalidFingerprint": "Saisissez l'empreinte SHA256 du certificat : 64 caractères hexadécimaux, éventuellement séparés par des deux-points.",
"fingerprintMismatch": "Le certificat PBS ne correspond pas à l'empreinte. Aucun identifiant n'a été envoyé.",
"tokenRejected": "PBS a refusé le jeton API ou ses permissions. Vérifiez les ACL de l'utilisateur et du jeton.",
"invalidResponse": "PBS a renvoyé une réponse inattendue. Les redirections ne sont pas suivies.",
"invalidJson": "PBS a renvoyé un document JSON non valide.",
"connectFailed": "Impossible de se connecter à PBS en HTTPS vérifié. Vérifiez l'adresse, le certificat et le service.",
"invalidId": "L'ID de la connexion PBS n'est pas valide. Rechargez la liste des connexions.",
"invalidDatastores": "PBS a renvoyé une liste de magasins de données non valide.",
"connectionLimit": "Vous pouvez configurer au maximum {count} connexions PBS.",
"username": "Saisissez l'utilisateur PBS au format user@realm, par exemple root@pam.",
"password": "Saisissez le mot de passe de l'utilisateur PBS.",
"loginRejected": "PBS a refusé le nom d'utilisateur ou le mot de passe.",
"invalidLogin": "PBS a renvoyé une réponse de connexion non valide.",
"tokenCreateDenied": "Cet utilisateur PBS ne peut pas créer de jetons API.",
"invalidToken": "PBS a renvoyé un jeton API non valide.",
"auditRoleDenied": "Cet utilisateur PBS ne peut pas attribuer le rôle Audit. Connectez-vous en tant que root@pam ou avec un autre administrateur.",
"notFound": "La connexion PBS est introuvable. Rechargez la liste des connexions.",
"readFailed": "Impossible de lire les données PBS. Vérifiez son service et les permissions.",
"invalidList": "PBS a renvoyé une liste de données non valide.",
"localUnavailable": "PBS n'est pas disponible sur cet hôte.",
"authRequired": "Activez l'authentification locale dans Monitor avant de gérer des connexions PBS distantes.",
"datastoreOffline": "Le magasin de données est hors ligne, en mode maintenance, ou son périphérique de stockage n'est pas monté.",
"datastoreUnavailable": "Le système de fichiers du magasin de données n'est pas disponible."
},
"taskTypes": {
"backup": "Sauvegarde",
"restore": "Restauration",
"verify": "Vérification",
"verifyGroup": "Vérification du groupe de sauvegardes",
"verifySnapshot": "Vérification de l'instantané",
"prune": "Purge",
"garbageCollection": "Ramasse-miettes",
"sync": "Synchronisation",
"updateCheck": "Recherche de mises à jour",
"terminal": "Terminal",
"createDatastore": "Créer un magasin de données",
"other": "Autre tâche ({type})"
},
"states": {
"ok": "OK",
"unknown": "Inconnu",
"warnings": "Avertissements : {count}",
"warning": "Avertissement",
"error": "Erreur"
},
"serviceStates": {
"active": "Actif",
"inactive": "Inactif",
"failed": "Échoué",
"activating": "Démarrage",
"deactivating": "Arrêt",
"unknown": "Inconnu"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} j"
},
"urlHelp": "Incluez :8007 pour le port PBS par défaut, par exemple https://pbs.example:8007.",
"usernameHelp": "Utilisez user@realm, par exemple root@pam.",
"tokenIdHelp": "Utilisez user@realm!token, par exemple monitor@pbs!proxmenux.",
"tokenSecretHelp": "Saisissez le secret de ce jeton API.",
"tokenSecretKeepHelp": "Laissez vide pour conserver le secret du jeton enregistré.",
"removeFailed": "Impossible de supprimer la connexion PBS.",
"savedRefreshFailed": "La connexion PBS a été enregistrée, mais la liste des connexions n'a pas pu être actualisée. Rechargez la page avant de tenter une autre modification.",
"removedRefreshFailed": "La connexion PBS a été supprimée, mais la liste des connexions n'a pas pu être actualisée. Rechargez la page.",
"technicalDetails": "Détails techniques",
"datastoreType": "Magasin de données"
},
"monitorLinks": {
"groups": {
@@ -7408,6 +7764,17 @@
"addSelected": "Ajouter la sélection",
"pbsWebInterface": "Interface web de PBS",
"changeAddress": "Modifier l'adresse",
"cancel": "Annuler"
"cancel": "Annuler",
"errors": {
"addressRequired": "Saisissez l'adresse du Monitor.",
"invalidAddress": "L'adresse doit commencer par http:// ou https:// et indiquer un hôte.",
"addressCredentials": "L'adresse ne doit contenir ni utilisateur ni mot de passe.",
"invalidPort": "Le port de l'adresse n'est pas valide.",
"invalidRequest": "La requête n'est pas valide. Rechargez la page et réessayez.",
"limit": "Vous pouvez ajouter au maximum {count} nœuds.",
"duplicate": "Cette adresse figure déjà dans la liste.",
"notFound": "Le nœud est introuvable. Rechargez la liste et réessayez.",
"detectedRemove": "Seuls les nœuds ajoutés manuellement peuvent être supprimés. Les nœuds détectés peuvent être masqués."
}
}
}
+375 -8
View File
@@ -1231,7 +1231,9 @@
"update": "Aggiorna",
"modify": "Modifica",
"recover": "Recupera",
"registerRestored": "Registra di nuovo",
"terminalTitleUpdate": "Aggiorna applicazione OCI",
"terminalTitleRecoverPrevious": "Ripristina l'installazione OCI precedente",
"terminalTitleModify": "Modifica applicazione OCI",
"terminalTitleRecover": "Recupera applicazione OCI ripristinata",
"terminalDescription": "Lo stesso percorso di OCI manager Apps → Gestisci le applicazioni OCI installate.",
@@ -1760,7 +1762,128 @@
"dockerEvidenceImage": "Immagine",
"dockerEvidencePort": "Porta TCP pubblicata",
"dockerEvidenceProject": "Progetto Compose",
"dockerEndpointReview": "Container con porte TCP pubblicate. Conferma il nome e l'indirizzo web prima di salvare. La registrazione non configura gli aggiornamenti."
"dockerEndpointReview": "Container con porte TCP pubblicate. Conferma il nome e l'indirizzo web prima di salvare. La registrazione non configura gli aggiornamenti.",
"confirmRemove": "Rimuovere il record di questa applicazione dalla scheda App? L'applicazione non viene disinstallata.",
"packageExample": "es. {package}",
"packageHelpBefore": "Compilato automaticamente dal nome. Verifica con",
"packageHelpAfter": "all'interno del CT.",
"binaryHelpBefore": "Percorso assoluto. Trovalo con",
"binaryHelpOr": "o",
"errors": {
"ociBusy": "è in corso un'operazione OCI su questo container",
"dpkgMissing": "{package} non è installato tramite dpkg",
"dpkgFailed": "dpkg-query non riuscito",
"dpkgNoVersion": "nessuna versione trovata nell'output di dpkg",
"apkFailed": "apk info non riuscito",
"apkNoVersion": "nessuna versione trovata nell'output di apk",
"fileRead": "impossibile leggere {path}",
"fileNoMatch": "file_regex non corrisponde a {path}",
"binaryFailed": "invocazione del binario non riuscita",
"binaryNoVersion": "nessuna versione trovata nell'output del binario",
"pythonFailed": "python -c importlib.metadata non riuscito",
"pythonNoVersion": "nessuna versione trovata nell'output della distribuzione Python",
"dockerImageResolve": "impossibile risolvere l'immagine installata del container",
"dockerInspectFailed": "docker inspect non riuscito",
"dockerLabelMissing": "il container non ha l'etichetta {label}",
"dockerLabelMutable": "l'etichetta Docker riporta {tag} (tag mobile, non una versione)",
"dockerLabelNoVersion": "nessuna versione trovata nell'output dell'etichetta Docker",
"dockerExecFailed": "docker exec non riuscito",
"dockerExecNoVersion": "nessuna versione trovata nell'output di docker exec",
"commandEmpty": "command_argv è vuoto",
"commandFailed": "comando non riuscito",
"commandNoVersion": "nessuna versione trovata nell'output del comando",
"unsupportedMethod": "metodo non supportato: {method}",
"unknownUpstream": "upstream_type sconosciuto: {type}",
"githubNotFound": "{repo}: non trovato",
"githubRejected": "GitHub ha rifiutato la richiesta (403)",
"githubError": "errore GitHub {code}",
"noRelease": "nessun nome di tag / release nella risposta",
"tagNoMatch": "tag_regex non corrisponde a '{tag}'",
"httpError": "errore HTTP {code}",
"invalidJson": "risposta JSON non valida: {detail}",
"jsonPathUnresolved": "json_path '{path}' non è stato risolto",
"jsonEmpty": "valore vuoto in json_path",
"dockerNoTags": "nessun tag corrispondente (docker_hub)",
"dockerMovingOnly": "tag_regex corrisponde solo a tag mobili; usa il monitoraggio tramite digest dell'immagine Docker",
"dockerHubError": "errore Docker Hub {code}",
"dockerHubJson": "JSON di Docker Hub non valido: {detail}",
"dockerHubEmpty": "nessun tag restituito da Docker Hub",
"payload": "il payload deve essere un oggetto JSON",
"name": "il nome è obbligatorio e deve contenere da 1 a 64 caratteri tra lettere/cifre/spazi/._+-()/",
"method": "installed_via deve essere uno tra: {values} oppure vuoto",
"package": "il pacchetto è obbligatorio (lettere/cifre/._+@:/ fino a 127 caratteri)",
"filePath": "file_path è obbligatorio e deve essere un percorso assoluto",
"fileRegex": "file_regex è obbligatorio",
"fileRegexInvalid": "file_regex non è una regex valida: {detail}",
"binaryPath": "binary_path è obbligatorio e deve essere un percorso assoluto",
"pythonPath": "python_path è obbligatorio e deve essere un percorso assoluto",
"distribution": "la distribuzione è obbligatoria (nome PEP 503)",
"container": "container_name è obbligatorio (regole di denominazione Docker)",
"label": "l'etichetta è obbligatoria (regole di denominazione delle etichette OCI)",
"dockerBinary": "binary_path è obbligatorio (percorso assoluto o comando semplice)",
"manualVersion": "installed_version è obbligatorio per il metodo manuale",
"versionLength": "installed_version supera {max} caratteri",
"versionControl": "installed_version contiene un carattere di controllo non consentito",
"installedRegexInvalid": "installed_regex non è una regex valida: {detail}",
"upstreamType": "upstream_type deve essere uno tra: {values}",
"tagRegexInvalid": "tag_regex non è una regex valida: {detail}",
"repoRequired": "repo è obbligatorio per upstream_type=github",
"repoFormat": "repo deve essere nel formato 'owner/name'",
"githubSource": "github_source deve essere uno tra: {values}",
"upstreamUrlRequired": "upstream_url è obbligatorio per upstream_type=http_json",
"upstreamUrl": "upstream_url deve essere un URL http(s)",
"upstreamUrlLength": "upstream_url supera {max} caratteri",
"jsonPathRequired": "upstream_json_path è obbligatorio per upstream_type=http_json",
"jsonPathLength": "upstream_json_path supera {max} caratteri",
"jsonPathCharacters": "upstream_json_path usa caratteri non consentiti",
"dockerImageRequired": "docker_image è obbligatorio per upstream_type=docker_hub",
"dockerImageLength": "docker_image supera {max} caratteri",
"dockerImageFormat": "docker_image deve rispettare la denominazione di Docker Hub (owner/name o name)",
"healthPath": "health_path deve essere un percorso URL valido (max 255 caratteri)",
"logoUrl": "logo_url deve essere un URL http(s) o l'icona locale in cache di questa VM",
"discoveryId": "discovery_id non valido",
"helperSlug": "helper_slug deve essere uno slug in minuscolo (lettere/cifre/._-)",
"updateVia": "update_via accetta solo 'docker'",
"dockerDetector": "update_via=docker richiede un rilevatore docker_label o docker_exec",
"dockerConflict": "update_via=docker non può essere combinato con {field}",
"updateCommandType": "update_command deve essere una stringa",
"updateCommandLength": "update_command supera {max} caratteri",
"updateCommandNull": "update_command contiene un byte nullo",
"updateMethod": "update_method deve essere none, helper o custom",
"updateCommandRequired": "update_command è obbligatorio per update_method=custom",
"updateCommandOnlyCustom": "update_command è consentito solo per update_method=custom",
"helperRequired": "per update_method=helper è obbligatorio un helper_slug supportato",
"dockerUpdater": "Le app delegate a Docker si aggiornano tramite la loro immagine, non con un programma di aggiornamento separato",
"commandRequired": "command_argv è obbligatorio (elenco non vuoto)",
"commandList": "command_argv deve essere un elenco non vuoto di stringhe",
"commandCount": "command_argv accetta al massimo {max} voci",
"commandEntry": "command_argv[{index}] deve essere una stringa non vuota",
"commandEntryLength": "command_argv[{index}] supera {max} caratteri",
"commandEntryControl": "command_argv[{index}] contiene un carattere di controllo non consentito",
"binaryArgsList": "binary_args deve essere un elenco di stringhe",
"binaryArgsCount": "binary_args accetta al massimo {max} voci",
"binaryArgEntry": "binary_args[{index}] deve essere una stringa non vuota",
"binaryArgLength": "binary_args[{index}] supera {max} caratteri",
"binaryArgControl": "binary_args[{index}] contiene un carattere di controllo non consentito",
"portsList": "ports deve essere un elenco di {port, description, scheme}",
"portObject": "ports[{index}] deve essere un oggetto",
"portRequired": "ports[{index}].port è obbligatorio",
"portInteger": "ports[{index}].port deve essere un numero intero",
"portRange": "ports[{index}].port deve essere compreso tra 1 e 65535",
"portDuplicate": "la porta {port} compare più di una volta per questa app",
"portDescription": "ports[{index}].description contiene caratteri non validi",
"portScheme": "ports[{index}].scheme deve essere 'http' o 'https'",
"portPath": "ports[{index}].web_path deve essere un percorso URL valido (max 255 caratteri)",
"portLogo": "ports[{index}].logo_url deve essere un URL http(s) (max 512 caratteri)",
"portCategory": "ports[{index}].category contiene caratteri non validi o è troppo lungo",
"portCustomUrl": "ports[{index}].custom_url deve essere un URL http(s) (max 512 caratteri)",
"tagRegexLength": "tag_regex supera 512 caratteri",
"persistPermission": "impossibile salvare il file sidecar (permessi?)",
"persist": "impossibile salvare il file sidecar",
"noApps": "nessuna app registrata per questo vmid",
"appMissing": "app_id '{appId}' non trovato",
"appNotFound": "app non trovata"
}
},
"statusFilter": {
"ariaLabel": "Filtra macchine virtuali e container",
@@ -2294,6 +2417,42 @@
"updateManyDescription": "Riesecuzione di {count} funzioni di post-installazione in sequenza.",
"legacySourceTitle": "Ottimizzazione legacy: fai clic per visualizzare il sorgente",
"sourceTitle": "Fai clic per vedere il codice sorgente",
"sourceDialogTitle": "Codice sorgente: {name}",
"sourceLoading": "Caricamento del codice sorgente…",
"sourceUnavailable": "Il codice sorgente non è disponibile.",
"sourceLoadFailed": "Impossibile caricare il codice sorgente. Riprova.",
"sourceFunctionMissing": "La funzione non è stata trovata negli script disponibili.",
"sourceUnknownTool": "Strumento sconosciuto: {tool}",
"sourceNoMapping": "Nessuna funzione è assegnata allo strumento {tool}.",
"sourceToolNames": {
"subscription_banner": "Rimozione del banner di abbonamento",
"time_sync": "Sincronizzazione dell'ora",
"apt_languages": "Salto delle lingue APT",
"journald": "Ottimizzazione di Journald",
"logrotate": "Ottimizzazione di Logrotate",
"system_limits": "Aumento dei limiti di sistema",
"memory_settings": "Ottimizzazione delle impostazioni di memoria",
"zram_swap": "Profilo di scambio ZRAM",
"kernel_panic": "Configurazione del kernel panic",
"apt_ipv4": "Forzatura IPv4 per APT",
"kexec": "kexec per riavvii rapidi",
"rpc": "Disattivazione di RPC / rpcbind",
"motd": "Banner MOTD personalizzato",
"system_utils": "Utilità di sistema",
"network_optimization": "Ottimizzazioni di rete",
"bashrc_custom": "Personalizzazione di Bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (protezione SSD)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Correzioni CPU AMD (Ryzen/EPYC)",
"persistent_network": "Configurazione di interfacce di rete persistenti",
"vfio_iommu": "Passthrough VFIO/IOMMU",
"lvm_repair": "Riparazione delle intestazioni PV LVM",
"repo_cleanup": "Pulizia dei repository",
"proxmox_repos": "Repository APT di Proxmox",
"entropy": "Generazione di entropia (haveged)"
},
"legacy": "legacy",
"updateToolTitle": "Aggiorna {name} a v{version}",
"availableUpdates": "Aggiornamenti disponibili",
@@ -4820,6 +4979,124 @@
"cancel": "Annulla",
"save": "Salva conservazione",
"useDefault": "Usa l'impostazione predefinita di Proxmox (nessuna policy su questo storage)"
},
"apiErrors": {
"requestFailed": "La richiesta non è riuscita.",
"absolutePath": "il percorso deve essere assoluto",
"pathConfigured": "questo percorso è già configurato",
"pathRequired": "il percorso è obbligatorio",
"pathQuery": "il parametro di query path è obbligatorio",
"pathNotFound": "percorso non trovato",
"invalidJobId": "ID del job non valido",
"jobNotFound": "job non trovato",
"runnerMissing": "script di esecuzione non installato",
"noRestore": "nessuno stato di ripristino da chiudere",
"invalidFile": "nome file non valido",
"notFound": "non trovato",
"logOutside": "percorso del log al di fuori della directory consentita",
"targetsList": "targets deve essere un elenco",
"emptyExpression": "espressione vuota",
"invalidId": "ID non valido (usa lettere, cifre, _ o -)",
"backend": "il backend deve essere pbs, local o borg",
"remoteBackend": "il backend deve essere pbs o borg",
"remoteBackendQuoted": "il backend deve essere 'pbs' o 'borg'",
"profileMode": "profile_mode deve essere default o custom",
"borgAttached": "borg non può essere collegato (vzdump di PVE scrive solo su storage pbs/local)",
"borgAttachedLong": "borg non è un backend valido per i job collegati (vzdump di PVE scrive solo su storage pbs/local)",
"parentRequired": "pve_parent_job_id e pve_storage sono obbligatori per attached",
"absolutePaths": "paths deve essere un elenco di percorsi assoluti",
"customPath": "il profilo personalizzato richiede almeno un percorso",
"defaultPaths": "impossibile determinare i percorsi del profilo predefinito",
"retentionObject": "retention deve essere un oggetto",
"pbsRepository": "pbs_repository è obbligatorio",
"pbsEncryption": "pbs_encrypt_mode deve essere 'none', 'new' o 'existing'",
"pbsKeyFailed": "impossibile creare il file di chiavi di crittografia PBS",
"pbsClientMissing": "impossibile creare il file di chiavi di crittografia PBS (proxmox-backup-client è installato?)",
"borgRepository": "borg_repo è obbligatorio",
"borgEncryption": "borg_encrypt_mode deve essere uno tra: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode deve essere uno tra: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase è obbligatoria quando la crittografia è abilitata (salvala nella destinazione o passane una nella richiesta)",
"passphrase": "la passphrase è obbligatoria quando la crittografia è abilitata",
"password": "la password è obbligatoria",
"deviceConfirmation": "confirm_device deve corrispondere esattamente a device (controllo di sicurezza / refusi)",
"invalidDevice": "percorso del dispositivo non valido",
"absoluteMountpoint": "è richiesto un punto di montaggio assoluto",
"archiveLayout": "struttura dell'archivio non riconosciuta: nessuna rootfs/ trovata",
"archiveMissing": "archivio non trovato",
"archiveNotFile": "il percorso dell'archivio non è un file regolare",
"invalidName": "nome non valido",
"invalidStaging": "staging_path non valido",
"jobRename": "il nome del job non può essere modificato (elimina e ricrea per rinominarlo)",
"absoluteKey": "key_path deve essere assoluto",
"mode": "mode deve essere \"local\" o \"ssh\"",
"nameRequired": "il nome è obbligatorio (lettere, cifre, _ o -)",
"noLog": "ancora nessun log disponibile per questo job",
"noPbsConfig": "nessun file di configurazioni PBS manuali",
"outputMissing": "file di output mancante",
"parserJson": "l'output del parser non era un JSON valido",
"restoreJson": "l'output di run_restore non era un JSON valido",
"repoRequired": "repo è obbligatorio",
"repoSnapshot": "repo_name e snapshot sono obbligatori",
"borgRepoSnapshot": "repo_name e snapshot sono obbligatori per borg",
"pbsRepoSnapshot": "repo_name e snapshot sono obbligatori per pbs",
"repoName": "repo_name è obbligatorio",
"restoreMissing": "strumenti di ripristino non installati su questo host",
"serverDatastore": "server e datastore sono obbligatori",
"snapshotRequired": "lo snapshot è obbligatorio",
"source": "source deve essere pbs, borg o local",
"sshPort": "ssh_port deve essere un numero intero tra 1 e 65535",
"sshRequired": "ssh_user, ssh_host e ssh_remote_path sono obbligatori per la modalità ssh",
"repositoryPath": "la destinazione non ha un percorso di repository",
"taskExpired": "attività non trovata o scaduta",
"invalidExpression": "espressione non valida",
"noReport": "nessun report generato",
"parseManifestFailed": "parse_manifest è terminato con un codice diverso da zero",
"jobExists": "il job esiste già: {value}",
"pathMissing": "il percorso non esiste su questo host: {value}",
"deviceMissing": "il dispositivo non esiste: {value}",
"mountpoint": "non è un punto di montaggio: {value}",
"localArchiveMissing": "archivio locale non trovato: {value}",
"borgDestination": "Destinazione Borg '{value}' non trovata",
"pbsDestination": "Destinazione PBS '{value}' non trovata",
"pbsPassword": "nessuna password disponibile per il repository PBS '{value}'",
"borgRepoMissing": "Repository Borg \"{value}\" non configurato",
"pbsRepoMissing": "Repository PBS \"{value}\" non configurato",
"borgTargetMissing": "Destinazione Borg non trovata: {value}",
"pbsTargetMissing": "Repository PBS non trovato: {value}",
"pveJobMissing": "Job vzdump di PVE non trovato: {value}",
"statePersist": "impossibile salvare lo stato: {value}",
"sshDirectory": "impossibile preparare la directory ssh: {value}",
"logRead": "impossibile leggere il log: {value}",
"envUpdate": "impossibile aggiornare .env: {value}",
"pbsConfigWrite": "impossibile salvare la configurazione PBS: {value}",
"persist": "impossibile salvare: {value}",
"archiveRemove": "impossibile rimuovere l'archivio: {value}",
"start": "impossibile avviare: {value}",
"borgTargetsUpdate": "impossibile aggiornare borg-targets.txt: {value}",
"pbsTargetsUpdate": "impossibile aggiornare pbs-manual-configs.txt: {value}",
"unitsUpdate": "impossibile aggiornare le unità systemd: {value}",
"borgTargetsWrite": "impossibile scrivere borg-targets.txt: {value}",
"jobWrite": "impossibile scrivere i file del job: {value}",
"localTargetWrite": "impossibile scrivere local-target.conf: {value}",
"unitsWrite": "impossibile scrivere le unità systemd: {value}",
"format": "formattazione non riuscita: {value}",
"calendar": "on_calendar non valido: {value}",
"mount": "montaggio non riuscito: {value}",
"parserInvocation": "invocazione del parser non riuscita: {value}",
"preflightInvocation": "invocazione della verifica preliminare non riuscita: {value}",
"publicKeyMissing": "chiave pubblica non trovata dopo la generazione: {value}",
"retentionInteger": "retention.{value} deve essere un numero intero",
"retentionNegative": "retention.{value} non deve essere negativo",
"keygen": "ssh-keygen non riuscito: {value}",
"systemctl": "systemctl non riuscito: {value}",
"unmount": "umount non riuscito: {value}",
"taskIncomplete": "l'attività è nello stato \"{value}\", non completata",
"unknownKind": "tipo sconosciuto: {value}",
"unknownMode": "modalità sconosciuta \"{value}\"",
"defaultRemove": "{value} è il valore predefinito integrato e non può essere rimosso",
"defaultAdd": "{value} è il valore predefinito integrato; non è necessario aggiungerlo",
"keyImport": "Nessun file di chiavi PBS è installato in {value}. Importa prima il file di chiavi.",
"storageMismatch": "Lo storage del job PVE non corrisponde (il job usa {value}, il payload indica {other})"
}
},
"healthStatus": {
@@ -5320,6 +5597,7 @@
"websocketError": "Errore WebSocket",
"connectionClosed": "Connessione chiusa",
"completedSuccessfully": "Script completato con successo",
"cancelled": "Operazione annullata.",
"completedWithError": "Script terminato con codice di errore {code}",
"yes": "Sì",
"yourInput": "Input:",
@@ -7311,7 +7589,7 @@
"signInWith": "Accedi con",
"methodPassword": "Utente e password",
"methodToken": "Token API",
"username": "Utente (utente@realm)",
"username": "Utente",
"password": "Password",
"passwordHelp": "La password viene usata una sola volta per creare un token API di sola lettura (ruolo Audit) per questo Monitor. Viene salvato solo quel token, mai la password. Attiva prima l'autenticazione del Monitor.",
"cancel": "Annulla",
@@ -7324,10 +7602,10 @@
"editConnection": "Connessione da modificare",
"add": "Aggiungi una nuova connessione",
"name": "Nome",
"url": "URL HTTPS (includere :8007 per la porta nativa di PBS)",
"token_id": "ID del token API (user@realm!token)",
"token_secret": "Segreto del token API (lasciare vuoto per mantenere quello salvato)",
"fingerprint": "Impronta SHA256 del certificato (facoltativa)",
"url": "Indirizzo HTTPS",
"token_id": "ID del token API",
"token_secret": "Segreto del token API",
"fingerprint": "Impronta SHA256 (facoltativa)",
"tlsHelp": "Lascia vuota l'impronta per la normale verifica dell'autorità di certificazione e del nome host. Per un certificato autofirmato, copia la sua impronta SHA256 dalla console PBS o da un altro canale attendibile. L'impronta viene verificata prima dell'invio delle credenziali.",
"saved": "Connessione PBS verificata e salvata.",
"saveFailed": "Impossibile salvare la connessione PBS.",
@@ -7369,7 +7647,85 @@
"estimatedFull": "Pieno stimato",
"estimatedFullUnknown": "dati non ancora sufficienti",
"duration": "Durata",
"noJobs": "non è configurata alcuna pianificazione di sincronizzazione, verifica o eliminazione."
"noJobs": "non è configurata alcuna pianificazione di sincronizzazione, verifica o eliminazione.",
"errors": {
"configRead": "Impossibile leggere le connessioni PBS salvate. Controlla il file di configurazione e i permessi.",
"invalidConfig": "Le impostazioni della connessione PBS non sono valide. Controlla il modulo e riprova.",
"invalidField": "Un campo della connessione non è valido o è troppo lungo.",
"invalidUrl": "Usa un indirizzo PBS HTTPS senza credenziali incorporate, parametri di query o frammento.",
"nameRequired": "Inserisci un nome del server di massimo 128 caratteri.",
"tokenIdFormat": "Inserisci l'ID del token API PBS nel formato user@realm!token.",
"tokenSecret": "Inserisci un segreto del token API valido, senza spazi né caratteri di controllo.",
"invalidTokenId": "L'ID del token API deve contenere solo caratteri ASCII.",
"invalidFingerprint": "Inserisci l'impronta SHA256 del certificato: 64 caratteri esadecimali, facoltativamente separati da due punti.",
"fingerprintMismatch": "Il certificato PBS non corrisponde all'impronta. Non sono state inviate credenziali.",
"tokenRejected": "PBS ha rifiutato il token API o i suoi permessi. Controlla le ACL sia dell'utente sia del token.",
"invalidResponse": "PBS ha restituito una risposta inattesa. I reindirizzamenti non vengono seguiti.",
"invalidJson": "PBS ha restituito un documento JSON non valido.",
"connectFailed": "Impossibile connettersi a PBS tramite HTTPS verificato. Controlla l'indirizzo, il certificato e il servizio.",
"invalidId": "L'ID della connessione PBS non è valido. Ricarica l'elenco delle connessioni.",
"invalidDatastores": "PBS ha restituito un elenco di datastore non valido.",
"connectionLimit": "Si possono configurare al massimo {count} connessioni PBS.",
"username": "Inserisci l'utente PBS nel formato user@realm, ad esempio root@pam.",
"password": "Inserisci la password dell'utente PBS.",
"loginRejected": "PBS ha rifiutato il nome utente o la password.",
"invalidLogin": "PBS ha restituito una risposta di accesso non valida.",
"tokenCreateDenied": "Questo utente PBS non può creare token API.",
"invalidToken": "PBS ha restituito un token API non valido.",
"auditRoleDenied": "Questo utente PBS non può assegnare il ruolo Audit. Accedi come root@pam o con un altro amministratore.",
"notFound": "Connessione PBS non trovata. Ricarica l'elenco delle connessioni.",
"readFailed": "Impossibile leggere i dati PBS. Controlla il servizio e i permessi.",
"invalidList": "PBS ha restituito un elenco di dati non valido.",
"localUnavailable": "PBS non è disponibile su questo host.",
"authRequired": "Abilita l'autenticazione locale in Monitor prima di gestire le connessioni PBS remote.",
"datastoreOffline": "Il datastore è offline, in modalità manutenzione oppure il dispositivo su cui si basa non è montato.",
"datastoreUnavailable": "Il filesystem del datastore non è disponibile."
},
"taskTypes": {
"backup": "Backup",
"restore": "Ripristino",
"verify": "Verifica",
"verifyGroup": "Verifica del gruppo di backup",
"verifySnapshot": "Verifica dello snapshot",
"prune": "Pulizia",
"garbageCollection": "Garbage collection",
"sync": "Sincronizzazione",
"updateCheck": "Controllo aggiornamenti",
"terminal": "Terminale",
"createDatastore": "Creazione datastore",
"other": "Altra attività ({type})"
},
"states": {
"ok": "OK",
"unknown": "Sconosciuto",
"warnings": "Avvisi: {count}",
"warning": "Avviso",
"error": "Errore"
},
"serviceStates": {
"active": "Attivo",
"inactive": "Inattivo",
"failed": "Non riuscito",
"activating": "Avvio in corso",
"deactivating": "Arresto in corso",
"unknown": "Sconosciuto"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} g"
},
"urlHelp": "Includi :8007 per la porta predefinita di PBS, ad esempio https://pbs.example:8007.",
"usernameHelp": "Usa utente@realm, ad esempio root@pam.",
"tokenIdHelp": "Usa utente@realm!token, ad esempio monitor@pbs!proxmenux.",
"tokenSecretHelp": "Inserisci il segreto di questo token API.",
"tokenSecretKeepHelp": "Lascia vuoto per mantenere il segreto del token salvato.",
"removeFailed": "Impossibile rimuovere la connessione PBS.",
"savedRefreshFailed": "La connessione PBS è stata salvata, ma non è stato possibile aggiornare l'elenco delle connessioni. Ricarica la pagina prima di fare un'altra modifica.",
"removedRefreshFailed": "La connessione PBS è stata rimossa, ma non è stato possibile aggiornare l'elenco delle connessioni. Ricarica la pagina.",
"technicalDetails": "Dettagli tecnici",
"datastoreType": "Datastore"
},
"monitorLinks": {
"groups": {
@@ -7408,6 +7764,17 @@
"addSelected": "Aggiungi selezionati",
"pbsWebInterface": "Interfaccia web di PBS",
"changeAddress": "Cambia indirizzo",
"cancel": "Annulla"
"cancel": "Annulla",
"errors": {
"addressRequired": "Inserisci l'indirizzo del Monitor.",
"invalidAddress": "L'indirizzo deve iniziare con http:// o https:// e indicare un host.",
"addressCredentials": "L'indirizzo non deve contenere un utente o una password.",
"invalidPort": "La porta dell'indirizzo non è valida.",
"invalidRequest": "La richiesta non è valida. Ricarica la pagina e riprova.",
"limit": "Si possono aggiungere al massimo {count} nodi.",
"duplicate": "Questo indirizzo è già nell'elenco.",
"notFound": "Nodo non trovato. Ricarica l'elenco e riprova.",
"detectedRemove": "Si possono rimuovere solo i nodi aggiunti manualmente. I nodi rilevati possono essere nascosti."
}
}
}
+375 -8
View File
@@ -1231,7 +1231,9 @@
"update": "Atualizar",
"modify": "Modificar",
"recover": "Recuperar",
"registerRestored": "Registar novamente",
"terminalTitleUpdate": "Atualizar aplicação OCI",
"terminalTitleRecoverPrevious": "Recuperar a instalação OCI anterior",
"terminalTitleModify": "Modificar aplicação OCI",
"terminalTitleRecover": "Recuperar aplicação OCI restaurada",
"terminalDescription": "É executado o mesmo processo que no menu do ProxMenux: OCI manager Apps → Gerir aplicações OCI instaladas.",
@@ -1760,7 +1762,128 @@
"dockerEvidenceImage": "Imagem",
"dockerEvidencePort": "Porta TCP publicada",
"dockerEvidenceProject": "Projeto Compose",
"dockerEndpointReview": "Contentor com portas TCP publicadas. Confirme o nome e o endereço web antes de guardar. O registo não configura atualizações."
"dockerEndpointReview": "Contentor com portas TCP publicadas. Confirme o nome e o endereço web antes de guardar. O registo não configura atualizações.",
"confirmRemove": "Eliminar o registo desta aplicação do separador App? A aplicação não é desinstalada.",
"packageExample": "p. ex. {package}",
"packageHelpBefore": "Preenchido automaticamente a partir do Nome. Verifique com",
"packageHelpAfter": "dentro do CT.",
"binaryHelpBefore": "Caminho absoluto. Encontre-o com",
"binaryHelpOr": "ou",
"errors": {
"ociBusy": "está em curso uma operação OCI neste contentor",
"dpkgMissing": "{package} não está instalado através do dpkg",
"dpkgFailed": "dpkg-query falhou",
"dpkgNoVersion": "nenhuma versão encontrada na saída do dpkg",
"apkFailed": "apk info falhou",
"apkNoVersion": "nenhuma versão encontrada na saída do apk",
"fileRead": "não foi possível ler {path}",
"fileNoMatch": "file_regex não encontrou correspondência em {path}",
"binaryFailed": "a invocação do binário falhou",
"binaryNoVersion": "nenhuma versão encontrada na saída do binário",
"pythonFailed": "python -c importlib.metadata falhou",
"pythonNoVersion": "nenhuma versão encontrada na saída da distribuição Python",
"dockerImageResolve": "não foi possível resolver a imagem instalada do contentor",
"dockerInspectFailed": "docker inspect falhou",
"dockerLabelMissing": "o contentor não tem a etiqueta {label}",
"dockerLabelMutable": "a etiqueta do docker indica {tag} (tag mutável, não é uma versão)",
"dockerLabelNoVersion": "nenhuma versão encontrada na saída da etiqueta do docker",
"dockerExecFailed": "docker exec falhou",
"dockerExecNoVersion": "nenhuma versão encontrada na saída do docker exec",
"commandEmpty": "command_argv está vazio",
"commandFailed": "o comando falhou",
"commandNoVersion": "nenhuma versão encontrada na saída do comando",
"unsupportedMethod": "método não suportado: {method}",
"unknownUpstream": "upstream_type desconhecido: {type}",
"githubNotFound": "{repo}: não encontrado",
"githubRejected": "o github rejeitou o pedido (403)",
"githubError": "erro do github {code}",
"noRelease": "nenhuma tag / nome de release na resposta",
"tagNoMatch": "tag_regex não encontrou correspondência em '{tag}'",
"httpError": "erro http {code}",
"invalidJson": "resposta JSON inválida: {detail}",
"jsonPathUnresolved": "json_path '{path}' não foi resolvido",
"jsonEmpty": "valor vazio em json_path",
"dockerNoTags": "nenhuma tag encontrada (docker_hub)",
"dockerMovingOnly": "tag_regex só encontrou tags móveis; utilize o seguimento do digest da imagem Docker",
"dockerHubError": "erro do docker hub {code}",
"dockerHubJson": "JSON do docker hub inválido: {detail}",
"dockerHubEmpty": "o docker hub não devolveu nenhuma tag",
"payload": "o payload deve ser um objeto JSON",
"name": "name é obrigatório e deve ter 1-64 caracteres entre letras/dígitos/espaços/._+-()/",
"method": "installed_via deve ser um de: {values} ou vazio",
"package": "package é obrigatório (letras/dígitos/._+@:/ até 127 caracteres)",
"filePath": "file_path é obrigatório e deve ser um caminho absoluto",
"fileRegex": "file_regex é obrigatório",
"fileRegexInvalid": "file_regex não é uma regex válida: {detail}",
"binaryPath": "binary_path é obrigatório e deve ser um caminho absoluto",
"pythonPath": "python_path é obrigatório e deve ser um caminho absoluto",
"distribution": "distribution é obrigatório (nome PEP 503)",
"container": "container_name é obrigatório (regras de nomes do docker)",
"label": "label é obrigatório (regras de nomes de etiquetas OCI)",
"dockerBinary": "binary_path é obrigatório (caminho absoluto ou comando simples)",
"manualVersion": "installed_version é obrigatório para o método manual",
"versionLength": "installed_version excede {max} caracteres",
"versionControl": "installed_version contém um carácter de controlo proibido",
"installedRegexInvalid": "installed_regex não é uma regex válida: {detail}",
"upstreamType": "upstream_type deve ser um de: {values}",
"tagRegexInvalid": "tag_regex não é uma regex válida: {detail}",
"repoRequired": "repo é obrigatório para upstream_type=github",
"repoFormat": "repo deve ter o formato 'owner/name'",
"githubSource": "github_source deve ser um de: {values}",
"upstreamUrlRequired": "upstream_url é obrigatório para upstream_type=http_json",
"upstreamUrl": "upstream_url deve ser um URL http(s)",
"upstreamUrlLength": "upstream_url excede {max} caracteres",
"jsonPathRequired": "upstream_json_path é obrigatório para upstream_type=http_json",
"jsonPathLength": "upstream_json_path excede {max} caracteres",
"jsonPathCharacters": "upstream_json_path utiliza caracteres proibidos",
"dockerImageRequired": "docker_image é obrigatório para upstream_type=docker_hub",
"dockerImageLength": "docker_image excede {max} caracteres",
"dockerImageFormat": "docker_image deve seguir os nomes do Docker Hub (owner/name ou name)",
"healthPath": "health_path deve ser um caminho de URL válido (máx. 255 caracteres)",
"logoUrl": "logo_url deve ser um URL http(s) ou o ícone local em cache desta VM",
"discoveryId": "discovery_id inválido",
"helperSlug": "helper_slug deve ser um slug em minúsculas (letras/dígitos/._-)",
"updateVia": "update_via só aceita 'docker'",
"dockerDetector": "update_via=docker requer um detetor docker_label ou docker_exec",
"dockerConflict": "update_via=docker não pode ser combinado com {field}",
"updateCommandType": "update_command deve ser uma cadeia de texto",
"updateCommandLength": "update_command excede {max} caracteres",
"updateCommandNull": "update_command contém um byte nulo",
"updateMethod": "update_method deve ser none, helper ou custom",
"updateCommandRequired": "update_command é obrigatório para update_method=custom",
"updateCommandOnlyCustom": "update_command só é permitido para update_method=custom",
"helperRequired": "é necessário um helper_slug suportado para update_method=helper",
"dockerUpdater": "As aplicações delegadas no Docker atualizam-se através da sua imagem, não com um atualizador de aplicação separado",
"commandRequired": "command_argv é obrigatório (lista não vazia)",
"commandList": "command_argv deve ser uma lista não vazia de cadeias de texto",
"commandCount": "command_argv aceita no máximo {max} entradas",
"commandEntry": "command_argv[{index}] deve ser uma cadeia de texto não vazia",
"commandEntryLength": "command_argv[{index}] excede {max} caracteres",
"commandEntryControl": "command_argv[{index}] contém um carácter de controlo proibido",
"binaryArgsList": "binary_args deve ser uma lista de cadeias de texto",
"binaryArgsCount": "binary_args aceita no máximo {max} entradas",
"binaryArgEntry": "binary_args[{index}] deve ser uma cadeia de texto não vazia",
"binaryArgLength": "binary_args[{index}] excede {max} caracteres",
"binaryArgControl": "binary_args[{index}] contém um carácter de controlo proibido",
"portsList": "ports deve ser uma lista de {port, description, scheme}",
"portObject": "ports[{index}] deve ser um objeto",
"portRequired": "ports[{index}].port é obrigatório",
"portInteger": "ports[{index}].port deve ser um número inteiro",
"portRange": "ports[{index}].port deve estar entre 1 e 65535",
"portDuplicate": "a porta {port} aparece mais de uma vez para esta aplicação",
"portDescription": "ports[{index}].description tem caracteres inválidos",
"portScheme": "ports[{index}].scheme deve ser 'http' ou 'https'",
"portPath": "ports[{index}].web_path deve ser um caminho de URL válido (máx. 255 caracteres)",
"portLogo": "ports[{index}].logo_url deve ser um URL http(s) (máx. 512 caracteres)",
"portCategory": "ports[{index}].category tem caracteres inválidos ou é demasiado longo",
"portCustomUrl": "ports[{index}].custom_url deve ser um URL http(s) (máx. 512 caracteres)",
"tagRegexLength": "tag_regex excede 512 caracteres",
"persistPermission": "não foi possível guardar o sidecar (permissões?)",
"persist": "não foi possível guardar o sidecar",
"noApps": "nenhuma aplicação registada para este vmid",
"appMissing": "app_id '{appId}' não encontrado",
"appNotFound": "aplicação não encontrada"
}
},
"statusFilter": {
"ariaLabel": "Filtrar máquinas virtuais e contêineres",
@@ -2294,6 +2417,42 @@
"updateManyDescription": "Executando novamente as funções pós-instalação {count} em sequência.",
"legacySourceTitle": "Otimização legada – clique para ver a fonte",
"sourceTitle": "Clique para visualizar o código fonte",
"sourceDialogTitle": "Código-fonte: {name}",
"sourceLoading": "A carregar o código-fonte…",
"sourceUnavailable": "O código-fonte não está disponível.",
"sourceLoadFailed": "Não foi possível carregar o código-fonte. Tente novamente.",
"sourceFunctionMissing": "A função não foi encontrada nos scripts disponíveis.",
"sourceUnknownTool": "Ferramenta desconhecida: {tool}",
"sourceNoMapping": "Nenhuma função está atribuída à ferramenta {tool}.",
"sourceToolNames": {
"subscription_banner": "Remoção do banner de subscrição",
"time_sync": "Sincronização da hora",
"apt_languages": "Omitir idiomas do APT",
"journald": "Otimização do journald",
"logrotate": "Otimização do logrotate",
"system_limits": "Aumento dos limites do sistema",
"memory_settings": "Otimização das definições de memória",
"zram_swap": "Perfil de swap ZRAM",
"kernel_panic": "Configuração de kernel panic",
"apt_ipv4": "Forçar IPv4 no APT",
"kexec": "kexec para reinícios rápidos",
"rpc": "Desativar RPC / rpcbind",
"motd": "Banner MOTD personalizado",
"system_utils": "Utilitários do sistema",
"network_optimization": "Otimizações de rede",
"bashrc_custom": "Personalização do bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (proteção do SSD)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Correções para CPU AMD (Ryzen/EPYC)",
"persistent_network": "Definir interfaces de rede persistentes",
"vfio_iommu": "Passthrough VFIO/IOMMU",
"lvm_repair": "Reparação de cabeçalhos LVM PV",
"repo_cleanup": "Limpeza de repositórios",
"proxmox_repos": "Repositórios APT do Proxmox",
"entropy": "Geração de entropia (haveged)"
},
"legacy": "legado",
"updateToolTitle": "Atualizar {name} para v{version}",
"availableUpdates": "Atualizações disponíveis",
@@ -4820,6 +4979,124 @@
"cancel": "Cancelar",
"save": "Guardar retenção",
"useDefault": "Use o padrão Proxmox (nenhuma política neste armazenamento)"
},
"apiErrors": {
"requestFailed": "O pedido falhou.",
"absolutePath": "o caminho deve ser absoluto",
"pathConfigured": "este caminho já está configurado",
"pathRequired": "o caminho é obrigatório",
"pathQuery": "o parâmetro de consulta path é obrigatório",
"pathNotFound": "caminho não encontrado",
"invalidJobId": "ID de trabalho inválido",
"jobNotFound": "trabalho não encontrado",
"runnerMissing": "o script de execução não está instalado",
"noRestore": "nenhum estado de restauro para descartar",
"invalidFile": "nome de ficheiro inválido",
"notFound": "não encontrado",
"logOutside": "caminho do registo fora do diretório permitido",
"targetsList": "targets deve ser uma lista",
"emptyExpression": "expressão vazia",
"invalidId": "ID inválido (utilize letras, dígitos, _ ou -)",
"backend": "o backend deve ser pbs, local ou borg",
"remoteBackend": "o backend deve ser pbs ou borg",
"remoteBackendQuoted": "o backend deve ser 'pbs' ou 'borg'",
"profileMode": "profile_mode deve ser default ou custom",
"borgAttached": "o borg não pode ser anexado (o vzdump do PVE só escreve em armazenamentos pbs/local)",
"borgAttachedLong": "o borg não é um backend válido para trabalhos anexados (o vzdump do PVE só escreve em armazenamentos pbs/local)",
"parentRequired": "pve_parent_job_id e pve_storage são obrigatórios para attached",
"absolutePaths": "paths deve ser uma lista de caminhos absolutos",
"customPath": "o perfil personalizado precisa de pelo menos um caminho",
"defaultPaths": "não foi possível resolver os caminhos do perfil predefinido",
"retentionObject": "retention deve ser um objeto",
"pbsRepository": "pbs_repository é obrigatório",
"pbsEncryption": "pbs_encrypt_mode deve ser 'none', 'new' ou 'existing'",
"pbsKeyFailed": "falha ao criar o ficheiro de chave de encriptação do PBS",
"pbsClientMissing": "falha ao criar o ficheiro de chave de encriptação do PBS (o proxmox-backup-client está instalado?)",
"borgRepository": "borg_repo é obrigatório",
"borgEncryption": "borg_encrypt_mode deve ser um de: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode deve ser um de: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase é obrigatório quando a encriptação está ativada (guarde-o no destino ou indique um no pedido)",
"passphrase": "a frase-passe é obrigatória quando a encriptação está ativada",
"password": "a palavra-passe é obrigatória",
"deviceConfirmation": "confirm_device deve corresponder exatamente ao dispositivo (verificação de gralhas / segurança)",
"invalidDevice": "caminho de dispositivo inválido",
"absoluteMountpoint": "é necessário um ponto de montagem absoluto",
"archiveLayout": "estrutura do arquivo não reconhecida — não foi encontrado rootfs/",
"archiveMissing": "arquivo não encontrado",
"archiveNotFile": "o caminho do arquivo não é um ficheiro normal",
"invalidName": "nome inválido",
"invalidStaging": "staging_path inválido",
"jobRename": "o nome do trabalho não pode ser alterado (elimine e volte a criar para mudar o nome)",
"absoluteKey": "key_path deve ser absoluto",
"mode": "mode deve ser \"local\" ou \"ssh\"",
"nameRequired": "o nome é obrigatório (letras, dígitos, _ ou -)",
"noLog": "ainda não há registo disponível para este trabalho",
"noPbsConfig": "não existe ficheiro de configurações PBS manuais",
"outputMissing": "ficheiro de saída em falta",
"parserJson": "a saída do parser não era JSON válido",
"restoreJson": "a saída de run_restore não era JSON válido",
"repoRequired": "repo é obrigatório",
"repoSnapshot": "repo_name e snapshot são obrigatórios",
"borgRepoSnapshot": "repo_name e snapshot são obrigatórios para borg",
"pbsRepoSnapshot": "repo_name e snapshot são obrigatórios para pbs",
"repoName": "repo_name é obrigatório",
"restoreMissing": "as ferramentas de restauro não estão instaladas neste host",
"serverDatastore": "server e datastore são obrigatórios",
"snapshotRequired": "snapshot é obrigatório",
"source": "source deve ser pbs, borg ou local",
"sshPort": "ssh_port deve ser um número inteiro entre 1 e 65535",
"sshRequired": "ssh_user, ssh_host e ssh_remote_path são obrigatórios para o modo ssh",
"repositoryPath": "o destino não tem caminho de repositório",
"taskExpired": "tarefa não encontrada ou expirada",
"invalidExpression": "expressão inválida",
"noReport": "nenhum relatório emitido",
"parseManifestFailed": "parse_manifest terminou com um código diferente de zero",
"jobExists": "o trabalho já existe: {value}",
"pathMissing": "o caminho não existe neste host: {value}",
"deviceMissing": "o dispositivo não existe: {value}",
"mountpoint": "não é um ponto de montagem: {value}",
"localArchiveMissing": "arquivo local não encontrado: {value}",
"borgDestination": "Destino Borg '{value}' não encontrado",
"pbsDestination": "Destino PBS '{value}' não encontrado",
"pbsPassword": "nenhuma palavra-passe disponível para o repositório PBS '{value}'",
"borgRepoMissing": "Repositório Borg \"{value}\" não configurado",
"pbsRepoMissing": "Repositório PBS \"{value}\" não configurado",
"borgTargetMissing": "Destino Borg não encontrado: {value}",
"pbsTargetMissing": "Repositório PBS não encontrado: {value}",
"pveJobMissing": "Trabalho vzdump do PVE não encontrado: {value}",
"statePersist": "não é possível guardar o estado: {value}",
"sshDirectory": "não é possível preparar o diretório ssh: {value}",
"logRead": "não é possível ler o registo: {value}",
"envUpdate": "não foi possível atualizar o .env: {value}",
"pbsConfigWrite": "falha ao guardar a configuração PBS: {value}",
"persist": "falha ao guardar: {value}",
"archiveRemove": "falha ao eliminar o arquivo: {value}",
"start": "falha ao iniciar: {value}",
"borgTargetsUpdate": "falha ao atualizar borg-targets.txt: {value}",
"pbsTargetsUpdate": "falha ao atualizar pbs-manual-configs.txt: {value}",
"unitsUpdate": "falha ao atualizar as unidades systemd: {value}",
"borgTargetsWrite": "falha ao escrever borg-targets.txt: {value}",
"jobWrite": "falha ao escrever os ficheiros do trabalho: {value}",
"localTargetWrite": "falha ao escrever local-target.conf: {value}",
"unitsWrite": "falha ao escrever as unidades systemd: {value}",
"format": "a formatação falhou: {value}",
"calendar": "on_calendar inválido: {value}",
"mount": "a montagem falhou: {value}",
"parserInvocation": "a invocação do parser falhou: {value}",
"preflightInvocation": "a invocação da verificação prévia falhou: {value}",
"publicKeyMissing": "chave pública não encontrada após a geração: {value}",
"retentionInteger": "retention.{value} deve ser um número inteiro",
"retentionNegative": "retention.{value} não pode ser negativo",
"keygen": "ssh-keygen falhou: {value}",
"systemctl": "systemctl falhou: {value}",
"unmount": "umount falhou: {value}",
"taskIncomplete": "a tarefa está no estado \"{value}\", não concluída",
"unknownKind": "tipo desconhecido: {value}",
"unknownMode": "modo desconhecido \"{value}\"",
"defaultRemove": "{value} é a predefinição integrada e não pode ser eliminado",
"defaultAdd": "{value} é a predefinição integrada; não é necessário adicioná-lo",
"keyImport": "Não existe nenhum ficheiro de chave PBS instalado em {value}. Importe primeiro o ficheiro de chave.",
"storageMismatch": "O armazenamento do trabalho do PVE não corresponde (o trabalho utiliza {value}, o payload indica {other})"
}
},
"healthStatus": {
@@ -5320,6 +5597,7 @@
"websocketError": "Ocorreu um erro de WebSocket",
"connectionClosed": "Conexão fechada",
"completedSuccessfully": "Script concluído com sucesso",
"cancelled": "Operação cancelada.",
"completedWithError": "Script concluído com código de erro {code}",
"yes": "Sim",
"yourInput": "Sua entrada:",
@@ -7311,7 +7589,7 @@
"signInWith": "Iniciar sessão com",
"methodPassword": "Utilizador e palavra-passe",
"methodToken": "Token API",
"username": "Utilizador (utilizador@realm)",
"username": "Utilizador",
"password": "Palavra-passe",
"passwordHelp": "A palavra-passe é usada uma única vez para criar um token API só de leitura (função Audit) para este Monitor. Só esse token é guardado, nunca a palavra-passe. Ative primeiro a autenticação do Monitor.",
"cancel": "Cancelar",
@@ -7324,10 +7602,10 @@
"editConnection": "Ligação a editar",
"add": "Adicionar uma nova ligação",
"name": "Nome",
"url": "URL HTTPS (inclua :8007 para a porta nativa do PBS)",
"token_id": "ID do token API (user@realm!token)",
"token_secret": "Segredo do token API (deixe vazio para manter o segredo guardado)",
"fingerprint": "Impressão digital SHA256 do certificado (opcional)",
"url": "Endereço HTTPS",
"token_id": "ID do token API",
"token_secret": "Segredo do token API",
"fingerprint": "Impressão digital SHA256 (opcional)",
"tlsHelp": "Deixe a impressão digital vazia para a verificação normal da autoridade de certificação e do nome de anfitrião. Para um certificado autoassinado, copie a sua impressão digital SHA256 da consola PBS ou de outro canal de confiança. A impressão digital é verificada antes do envio de credenciais.",
"saved": "Ligação PBS verificada e guardada.",
"saveFailed": "Não foi possível guardar a ligação PBS.",
@@ -7369,7 +7647,85 @@
"estimatedFull": "estimativa completa",
"estimatedFullUnknown": "Not enough data yet",
"duration": "Duração",
"noJobs": "nenhuma programação de sincronização, verificação ou remoção está configurada."
"noJobs": "nenhuma programação de sincronização, verificação ou remoção está configurada.",
"errors": {
"configRead": "Não foi possível ler as ligações PBS guardadas. Verifique o ficheiro de configuração e as permissões.",
"invalidConfig": "As definições da ligação PBS não são válidas. Verifique o formulário e tente novamente.",
"invalidField": "Um campo da ligação não é válido ou é demasiado longo.",
"invalidUrl": "Utilize um endereço PBS HTTPS sem credenciais incorporadas, parâmetros de consulta nem fragmento.",
"nameRequired": "Introduza um nome de servidor com 128 caracteres no máximo.",
"tokenIdFormat": "Introduza o ID do token API do PBS no formato utilizador@realm!token.",
"tokenSecret": "Introduza um segredo de token API válido, sem espaços nem caracteres de controlo.",
"invalidTokenId": "O ID do token API só pode conter caracteres ASCII.",
"invalidFingerprint": "Introduza a impressão digital SHA256 do certificado: 64 caracteres hexadecimais, opcionalmente separados por dois pontos.",
"fingerprintMismatch": "O certificado do PBS não corresponde à impressão digital. Não foram enviadas credenciais.",
"tokenRejected": "O PBS rejeitou o token API ou as suas permissões. Verifique as ACL do utilizador e do token.",
"invalidResponse": "O PBS devolveu uma resposta inesperada. Os redirecionamentos não são seguidos.",
"invalidJson": "O PBS devolveu um documento JSON inválido.",
"connectFailed": "Não foi possível ligar ao PBS através de HTTPS verificado. Verifique o endereço, o certificado e o serviço.",
"invalidId": "O ID da ligação PBS não é válido. Recarregue a lista de ligações.",
"invalidDatastores": "O PBS devolveu uma lista de armazenamentos de dados inválida.",
"connectionLimit": "Podem ser configuradas no máximo {count} ligações PBS.",
"username": "Introduza o utilizador do PBS no formato utilizador@realm, por exemplo root@pam.",
"password": "Introduza a palavra-passe do utilizador do PBS.",
"loginRejected": "O PBS rejeitou o nome de utilizador ou a palavra-passe.",
"invalidLogin": "O PBS devolveu uma resposta de início de sessão inválida.",
"tokenCreateDenied": "Este utilizador do PBS não pode criar tokens API.",
"invalidToken": "O PBS devolveu um token API inválido.",
"auditRoleDenied": "Este utilizador do PBS não pode conceder a função Audit. Inicie sessão como root@pam ou outro administrador.",
"notFound": "A ligação PBS não foi encontrada. Recarregue a lista de ligações.",
"readFailed": "Não foi possível ler os dados do PBS. Verifique o seu serviço e as permissões.",
"invalidList": "O PBS devolveu uma lista de dados inválida.",
"localUnavailable": "O PBS não está disponível neste host.",
"authRequired": "Ative a autenticação local no Monitor antes de gerir ligações PBS remotas.",
"datastoreOffline": "O armazenamento de dados está offline, em modo de manutenção ou o seu dispositivo de suporte não está montado.",
"datastoreUnavailable": "O sistema de ficheiros do armazenamento de dados não está disponível."
},
"taskTypes": {
"backup": "Backup",
"restore": "Restauro",
"verify": "Verificação",
"verifyGroup": "Verificação do grupo de backups",
"verifySnapshot": "Verificação do snapshot",
"prune": "Limpeza (prune)",
"garbageCollection": "Recolha de lixo",
"sync": "Sincronização",
"updateCheck": "Procurar atualizações",
"terminal": "Terminal",
"createDatastore": "Criar armazenamento de dados",
"other": "Outra tarefa ({type})"
},
"states": {
"ok": "OK",
"unknown": "Desconhecido",
"warnings": "Avisos: {count}",
"warning": "Aviso",
"error": "Erro"
},
"serviceStates": {
"active": "Ativo",
"inactive": "Inativo",
"failed": "Com falha",
"activating": "A iniciar",
"deactivating": "A parar",
"unknown": "Desconhecido"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} d"
},
"urlHelp": "Inclua :8007 para a porta predefinida do PBS, por exemplo https://pbs.example:8007.",
"usernameHelp": "Utilize utilizador@realm, por exemplo root@pam.",
"tokenIdHelp": "Utilize utilizador@realm!token, por exemplo monitor@pbs!proxmenux.",
"tokenSecretHelp": "Introduza o segredo deste token API.",
"tokenSecretKeepHelp": "Deixe vazio para manter o segredo do token guardado.",
"removeFailed": "Não foi possível eliminar a ligação PBS.",
"savedRefreshFailed": "A ligação PBS foi guardada, mas não foi possível atualizar a lista de ligações. Recarregue a página antes de tentar outra alteração.",
"removedRefreshFailed": "A ligação PBS foi eliminada, mas não foi possível atualizar a lista de ligações. Recarregue a página.",
"technicalDetails": "Detalhes técnicos",
"datastoreType": "Armazenamento de dados"
},
"monitorLinks": {
"groups": {
@@ -7408,6 +7764,17 @@
"addSelected": "Adicionar selecionados",
"pbsWebInterface": "Interface web do PBS",
"changeAddress": "Alterar endereço",
"cancel": "Cancelar"
"cancel": "Cancelar",
"errors": {
"addressRequired": "Introduza o endereço do Monitor.",
"invalidAddress": "O endereço deve começar por http:// ou https:// e indicar um host.",
"addressCredentials": "O endereço não pode conter um utilizador nem uma palavra-passe.",
"invalidPort": "A porta do endereço não é válida.",
"invalidRequest": "O pedido não é válido. Recarregue a página e tente novamente.",
"limit": "Podem ser adicionados no máximo {count} nós.",
"duplicate": "Este endereço já está na lista.",
"notFound": "O nó não foi encontrado. Recarregue a lista e tente novamente.",
"detectedRemove": "Só podem ser eliminados os nós adicionados manualmente. Os nós detetados podem ser ocultados."
}
}
}
+408 -66
View File
@@ -8,7 +8,7 @@
"notAvailable": "Nedostupné",
"serverOffline": "Server je offline",
"supportProject": "Podporte projekt alebo prispejte vlastnými úpravami",
"descriptionPbs": "Proxmox Backup Server Dashboard"
"descriptionPbs": "Prehľad systému Proxmox Backup Server"
},
"common": {
"notAvailable": "Nedostupné",
@@ -1032,7 +1032,7 @@
"pciPassthrough": "PCI Passthrough",
"usbDevices": "USB zariadenia",
"serialPorts": "Sériové porty",
"bridge": "Sieťový bridge",
"bridge": "Sieťový most (bridge)",
"enabled": "Zapnuté",
"disabled": "Vypnuté",
"on": "zapnuté",
@@ -1248,13 +1248,15 @@
"stackNote": "Súčasť aplikácie s viacerými kontajnermi (hlavný CT {primary}, {count} kontajnerov): aktualizácia sa týka všetkých.",
"memberNote": "Tento kontajner patrí do aplikácie s viacerými kontajnermi, ktorej hlavný CT je {primary}, a neaktualizuje sa samostatne. Aktualizácia sa vykonáva z hlavného CT, ktorý aktualizuje všetkých {count} kontajnerov naraz.",
"openPrimary": "Otvoriť hlavný CT",
"pendingNote": "Operácia na tomto kontajneri bola prerušená; tlačidlo Obnoviť spustí obnovenie.",
"pendingNote": "Operácia na tomto kontajneri zostala nedokončená. Tlačidlo Obnoviť otvorí možnosti obnovenia.",
"update": "Aktualizovať",
"modify": "Upraviť",
"recover": "Obnoviť",
"registerRestored": "Znova zaregistrovať",
"terminalTitleUpdate": "Aktualizovať OCI aplikáciu",
"terminalTitleRecoverPrevious": "Obnoviť predchádzajúcu OCI inštaláciu",
"terminalTitleModify": "Upraviť OCI aplikáciu",
"terminalTitleRecover": "Obnoviť obnovenú OCI aplikáciu",
"terminalTitleRecover": "Zaregistrovať OCI aplikácie obnovené zo zálohy",
"terminalDescription": "Použije sa rovnaký postup ako v ponuke Správca OCI aplikácií → Spravovať nainštalované OCI aplikácie.",
"keepBackup": "Ponechať zálohu vytvorenú pred aktualizáciou",
"noKeepBackup": "Záloha vytvorená pred aktualizáciou sa neponecháva",
@@ -1485,7 +1487,7 @@
"ociTitle": "Kontajner z OCI obrazu",
"ociBody": "Tento kontajner bol vytvorený z OCI (Docker) obrazu mimo Správcu OCI aplikácií. Aktualizuje sa nahradením obrazu, čo ProxMenux pri tomto kontajneri nespravuje.",
"ociRestoredTitle": "Obnovená OCI aplikácia",
"ociRestoredBody": "Tento kontajner bol obnovený zo zálohy a tento hostiteľ nemá záznam o jeho aplikácii, preto ho zatiaľ nemožno aktualizovať odtiaľto. Obnoviť ho znova zaregistruje na tomto hostiteľovi, rovnako ako OCI manager Apps → Spravovať nainštalované OCI aplikácie.",
"ociRestoredBody": "Tento kontajner bol obnovený zo zálohy, ale tento server nemá záznam o jeho OCI aplikácii, preto ju tu zatiaľ nemožno aktualizovať. Možnosť Znova zaregistrovať otvorí registráciu obnovených OCI aplikácií na tomto uzle, rovnako ako ponuka Správca OCI aplikácií → Spravovať nainštalované OCI aplikácie.",
"dockerImagesTitle": "Docker obrazy",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Obrazy",
@@ -1545,7 +1547,128 @@
"loadFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo načítať."
},
"appEditor": {
"methodOciImage": "OCI obraz (nainštalovaný ProxMenux)",
"confirmRemove": "Odobrať záznam aplikácie zo záložky Aplikácie? Samotná aplikácia zostane nainštalovaná.",
"packageExample": "napr. {package}",
"packageHelpBefore": "Doplní sa podľa názvu. Over ho príkazom",
"packageHelpAfter": "v kontajneri.",
"binaryHelpBefore": "Úplná cesta k programu. Nájdeš ju príkazom",
"binaryHelpOr": "alebo",
"errors": {
"ociBusy": "V tomto kontajneri práve prebieha operácia OCI.",
"dpkgMissing": "Balík {package} nie je nainštalovaný cez dpkg.",
"dpkgFailed": "Príkaz dpkg-query zlyhal.",
"dpkgNoVersion": "Vo výstupe dpkg sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"apkFailed": "Príkaz apk info zlyhal.",
"apkNoVersion": "Vo výstupe apk sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"fileRead": "Súbor {path} sa nepodarilo prečítať.",
"fileNoMatch": "Regulárny výraz file_regex nenašiel zhodu v súbore {path}.",
"binaryFailed": "Program sa nepodarilo spustiť.",
"binaryNoVersion": "Vo výstupe programu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"pythonFailed": "Zisťovanie verzie cez python -c importlib.metadata zlyhalo.",
"pythonNoVersion": "Vo výstupe balíka Pythonu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"dockerImageResolve": "Nainštalovaný obraz kontajnera sa nepodarilo určiť.",
"dockerInspectFailed": "Príkaz docker inspect zlyhal.",
"dockerLabelMissing": "Kontajner nemá štítok {label}.",
"dockerLabelMutable": "Štítok Dockeru uvádza {tag}. Je to premenlivý tag, ktorý neurčuje konkrétnu verziu.",
"dockerLabelNoVersion": "V štítku Dockeru sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"dockerExecFailed": "Príkaz docker exec zlyhal.",
"dockerExecNoVersion": "Vo výstupe docker exec sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"commandEmpty": "Príkaz a argumenty (command_argv) nie sú vyplnené.",
"commandFailed": "Príkaz zlyhal.",
"commandNoVersion": "Vo výstupe príkazu sa nenašla verzia zodpovedajúca regulárnemu výrazu.",
"unsupportedMethod": "Nepodporovaný spôsob zisťovania verzie: {method}",
"unknownUpstream": "Neznámy zdroj dostupnej verzie (upstream_type): {type}",
"githubNotFound": "Repozitár {repo} sa nenašiel.",
"githubRejected": "GitHub odmietol požiadavku (403).",
"githubError": "Chyba GitHubu {code}.",
"noRelease": "Odpoveď neobsahuje tag ani názov vydania.",
"tagNoMatch": "Regulárny výraz tag_regex nenašiel zhodu s hodnotou „{tag}“.",
"httpError": "Chyba HTTP {code}.",
"invalidJson": "Odpoveď neobsahuje platný JSON: {detail}",
"jsonPathUnresolved": "Cesta v JSON „{path}“ sa nenašla.",
"jsonEmpty": "Na zadanej ceste v JSON je prázdna hodnota.",
"dockerNoTags": "Na Docker Hube sa nenašli tagy zodpovedajúce filtru.",
"dockerMovingOnly": "Filtru tag_regex zodpovedajú iba premenlivé tagy. Použi sledovanie obrazov Docker podľa digestu.",
"dockerHubError": "Chyba Docker Hubu {code}.",
"dockerHubJson": "Docker Hub nevrátil platný JSON: {detail}",
"dockerHubEmpty": "Docker Hub nevrátil žiadne tagy.",
"payload": "Údaje musia byť objekt JSON.",
"name": "Zadaj názov s dĺžkou 1 až 64 znakov. Povolené sú písmená, číslice, medzery a ._+-()/.",
"method": "Spôsob zisťovania verzie (installed_via) musí byť jedna z hodnôt: {values}, alebo môže zostať prázdny.",
"package": "Zadaj názov balíka (package), najviac 127 znakov. Povolené sú písmená, číslice a ._+@:/.",
"filePath": "Zadaj úplnú cestu k súboru (file_path), začínajúcu lomkou /.",
"fileRegex": "Zadaj regulárny výraz na získanie verzie zo súboru (file_regex).",
"fileRegexInvalid": "Regulárny výraz pre súbor (file_regex) je neplatný: {detail}",
"binaryPath": "Zadaj úplnú cestu k programu (binary_path), začínajúcu lomkou /.",
"pythonPath": "Zadaj úplnú cestu k interpretu Pythonu (python_path), začínajúcu lomkou /.",
"distribution": "Zadaj názov balíka Pythonu (distribution) podľa PEP 503.",
"container": "Zadaj názov kontajnera (container_name) podľa pravidiel Dockeru.",
"label": "Zadaj názov štítka (label) podľa pravidiel OCI.",
"dockerBinary": "Zadaj cestu k programu alebo samotný názov príkazu (binary_path).",
"manualVersion": "Pri ručnom zadávaní vyplň nainštalovanú verziu (installed_version).",
"versionLength": "Nainštalovaná verzia (installed_version) môže mať najviac {max} znakov.",
"versionControl": "Nainštalovaná verzia (installed_version) obsahuje nepovolený riadiaci znak.",
"installedRegexInvalid": "Regulárny výraz pre nainštalovanú verziu (installed_regex) je neplatný: {detail}",
"upstreamType": "Zdroj dostupnej verzie (upstream_type) musí byť jedna z hodnôt: {values}.",
"tagRegexInvalid": "Regulárny výraz pre tagy (tag_regex) je neplatný: {detail}",
"repoRequired": "Pre zdroj GitHub zadaj repozitár (repo).",
"repoFormat": "Repozitár (repo) musí mať tvar „vlastník/názov“ (owner/name).",
"githubSource": "Zdroj na GitHube (github_source) musí byť jedna z hodnôt: {values}.",
"upstreamUrlRequired": "Pre zdroj HTTP JSON zadaj adresu (upstream_url).",
"upstreamUrl": "Adresa zdroja (upstream_url) musí začínať na http:// alebo https://.",
"upstreamUrlLength": "Adresa zdroja (upstream_url) môže mať najviac {max} znakov.",
"jsonPathRequired": "Pre zdroj HTTP JSON zadaj cestu k verzii v JSON (upstream_json_path).",
"jsonPathLength": "Cesta k verzii v JSON (upstream_json_path) môže mať najviac {max} znakov.",
"jsonPathCharacters": "Cesta k verzii v JSON (upstream_json_path) obsahuje nepovolené znaky.",
"dockerImageRequired": "Pre zdroj Docker Hub zadaj názov obrazu (docker_image).",
"dockerImageLength": "Názov obrazu (docker_image) môže mať najviac {max} znakov.",
"dockerImageFormat": "Názov obrazu (docker_image) musí mať tvar owner/name alebo name podľa pravidiel Docker Hubu.",
"healthPath": "Cesta na kontrolu stavu (health_path) musí byť platná cesta URL s najviac 255 znakmi.",
"logoUrl": "Logo (logo_url) musí byť adresa HTTP/HTTPS alebo lokálna ikona uložená pre tento stroj.",
"discoveryId": "Identifikátor nájdenej aplikácie (discovery_id) je neplatný.",
"helperSlug": "Identifikátor inštalačného skriptu (helper_slug) môže obsahovať malé písmená, číslice a ._-.",
"updateVia": "Pole update_via podporuje iba hodnotu docker.",
"dockerDetector": "Pri update_via=docker vyber detektor docker_label alebo docker_exec.",
"dockerConflict": "Pole {field} nemožno použiť spolu s update_via=docker.",
"updateCommandType": "Aktualizačný príkaz (update_command) musí byť text.",
"updateCommandLength": "Aktualizačný príkaz (update_command) môže mať najviac {max} znakov.",
"updateCommandNull": "Aktualizačný príkaz (update_command) obsahuje nepovolený nulový znak.",
"updateMethod": "Spôsob aktualizácie (update_method) musí byť none, helper alebo custom.",
"updateCommandRequired": "Pre vlastnú aktualizáciu zadaj príkaz (update_command).",
"updateCommandOnlyCustom": "Aktualizačný príkaz (update_command) je povolený iba pri update_method=custom.",
"helperRequired": "Pre update_method=helper vyber podporovaný inštalačný skript (helper_slug).",
"dockerUpdater": "Aplikácia v Dockeri sa aktualizuje spolu s obrazom. Samostatný aktualizátor aplikácie tu nemožno nastaviť.",
"commandRequired": "Zadaj príkaz a jeho argumenty (command_argv) ako neprázdny zoznam.",
"commandList": "Príkaz a argumenty (command_argv) musia byť neprázdny zoznam textových hodnôt.",
"commandCount": "Príkaz a argumenty (command_argv) môžu mať najviac {max} položiek.",
"commandEntry": "Položka command_argv[{index}] musí obsahovať text.",
"commandEntryLength": "Položka command_argv[{index}] môže mať najviac {max} znakov.",
"commandEntryControl": "Položka command_argv[{index}] obsahuje nepovolený riadiaci znak.",
"binaryArgsList": "Argumenty programu (binary_args) musia byť zoznam textových hodnôt.",
"binaryArgsCount": "Argumenty programu (binary_args) môžu mať najviac {max} položiek.",
"binaryArgEntry": "Argument binary_args[{index}] musí obsahovať text.",
"binaryArgLength": "Argument binary_args[{index}] môže mať najviac {max} znakov.",
"binaryArgControl": "Argument binary_args[{index}] obsahuje nepovolený riadiaci znak.",
"portsList": "Webové odkazy (ports) musia byť zoznam položiek s poľami port, description a scheme.",
"portObject": "Webový odkaz ports[{index}] musí byť objekt.",
"portRequired": "Vo webovom odkaze ports[{index}] zadaj port.",
"portInteger": "Port vo webovom odkaze ports[{index}] musí byť celé číslo.",
"portRange": "Port vo webovom odkaze ports[{index}] musí byť od 1 do 65535.",
"portDuplicate": "Port {port} je pri tejto aplikácii uvedený viackrát.",
"portDescription": "Popis webového odkazu ports[{index}] obsahuje nepovolené znaky.",
"portScheme": "Protokol webového odkazu ports[{index}] musí byť http alebo https.",
"portPath": "Cesta webového odkazu ports[{index}].web_path musí byť platná cesta URL s najviac 255 znakmi.",
"portLogo": "Logo webového odkazu ports[{index}].logo_url musí byť adresa HTTP/HTTPS s najviac 512 znakmi.",
"portCategory": "Kategória webového odkazu ports[{index}].category obsahuje nepovolené znaky alebo je príliš dlhá.",
"portCustomUrl": "Vlastná adresa webového odkazu ports[{index}].custom_url musí byť adresa HTTP/HTTPS s najviac 512 znakmi.",
"tagRegexLength": "Regulárny výraz pre tagy (tag_regex) môže mať najviac 512 znakov.",
"persistPermission": "Evidenciu aplikácie sa nepodarilo uložiť. Skontroluj oprávnenia na zápis.",
"persist": "Evidenciu aplikácie sa nepodarilo uložiť.",
"noApps": "Pre tento stroj nie sú evidované žiadne aplikácie.",
"appMissing": "Záznam aplikácie „{appId}“ sa nenašiel.",
"appNotFound": "Záznam aplikácie sa nenašiel."
},
"methodOciImage": "OCI obraz (nainštaloval ProxMenux)",
"ociImageHelp": "Verzia aj dostupné aktualizácie sa určujú podľa obrazu, z ktorého bol tento kontajner vytvorený. Keďže ho nainštaloval ProxMenux, netreba nič nastavovať: nainštalovaný obraz sa porovnáva s obrazom, ktorý je dnes dostupný v jeho registri.",
"ociAppVersion": "Aplikácia",
"ociImage": "Obraz",
@@ -1585,7 +1708,7 @@
"installedRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
"installedRegexAlt": "install_regex",
"regexCaptureGroupLabel": "Regex (so zachytávacou skupinou)",
"regexPlaceholderVersion": "verzia:\\s*(\\d+\\.\\d+\\.\\d+)",
"regexPlaceholderVersion": "version:\\s*(\\d+\\.\\d+\\.\\d+)",
"optionalSuffix": "(voliteľné)",
"methodNone": "Žiadny (iba odkaz)",
"methodDpkg": "balík dpkg (Debian/Ubuntu)",
@@ -1606,12 +1729,12 @@
"binaryArgsPlaceholder": "napr. --version (oddelené čiarkou)",
"filePathLabel": "Cesta k súboru",
"commandLabel": "Príkaz (argv oddelené čiarkou, bez shellu)",
"commandPlaceholder": "myapp, verzia, --json",
"commandPlaceholder": "myapp, version, --json",
"commandServerExample": "server, -v",
"containerNameLabel": "Názov kontajnera",
"containerNamePlaceholder": "napr. <your-container-name>",
"ociLabelKeyLabel": "Kľúč OCI labelu",
"pythonInterpreterLabel": "Cesta k Python interpreteru",
"ociLabelKeyLabel": "Kľúč štítka OCI",
"pythonInterpreterLabel": "Cesta k interpretu Pythonu",
"pythonInterpreterPlaceholder": "napr. /opt/<your-app>/venv/bin/python",
"pipDistLabel": "Názov distribúcie (pip balík)",
"pipDistPlaceholder": "napr. <your-pip-package>",
@@ -1651,7 +1774,7 @@
"latestFromLabel": "Najnovšia verzia z",
"upToDateBadge": "Aktuálne",
"updateAvailableBadge": "Dostupná aktualizácia",
"portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)",
"portDescriptionPlaceholder": "Popis (napr. webové rozhranie, go2rtc, správa)",
"portPortPlaceholder": "port",
"portHttp": "http",
"portHttps": "https",
@@ -1670,7 +1793,7 @@
"detectMethodDockerLabel": "štítok docker",
"detectMethodDockerExec": "docker exec",
"detectMethodCommand": "príkaz",
"binaryVersionHint": "binárna verzia",
"binaryVersionHint": "program (výstup --version)",
"systemctlHint": "systemctl show <service> -p ExecStart",
"whichHint": "ktorá <aplikácia>",
"apkInfoHint": "\" : \"informácie o apk | grep",
@@ -1721,7 +1844,7 @@
"detectorTestTitle": "Výsledok testu detektora",
"detectorTestNotSaved": "Iba koncept · neuložené",
"detectorTestNoVersion": "Nebola zistená žiadna verzia",
"detectorTestNoUpstream": "Nie je nastavený zdroj upstream",
"detectorTestNoUpstream": "Nie je nastavený zdroj dostupnej verzie",
"detectorTestFailed": "Test detektora zlyhal",
"deleteFailed": "Odstránenie zlyhalo",
"dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť",
@@ -1768,17 +1891,17 @@
"addAnotherApplication": "Registrovať ďalšiu aplikáciu",
"doneButton": "Hotovo",
"editButton": "Upraviť",
"upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
"upstreamErrorTimeout": "Zdroj dostupnej verzie neodpovedal včas.",
"upstreamErrorNetwork": "Chyba siete: {detail}",
"upstreamErrorGithubRateLimit": "Bol dosiahnutý limit požiadaviek GitHubu. V časti Nastavenia → GitHub API nakonfigurujte voliteľný token alebo to skúste znova neskôr.",
"upstreamErrorRegistry": "Register obrazu sa nepodarilo kontaktovať. O niekoľko minút sa skontroluje znova.",
"upstreamErrorGeneric": "Kontrola proti prúdu zlyhala: {detail}",
"notificationsEnabled": "Upozornenia na upstream aktualizácie sú ZAPNUTÉ – kliknutím ich stlmíte",
"notificationsMuted": "Upstream upozornenia na aktualizácie MUTED – kliknutím aktivujete",
"notifyUpstreamLabel": "Upozorniť ma, keď bude k dispozícii nová upstream verzia",
"notifyUpstreamHelp": "Odošle `app_update_available` do kanálov povolených v Nastaveniach → Upozornenia. Vypnite, ak túto aplikáciu nie je možné aktualizovať na vašom boxe.",
"notificationsEnabled": "Upozornenia na nové verzie sú zapnuté — kliknutím ich stlmíš",
"notificationsMuted": "Upozornenia na nové verzie sú stlmené — kliknutím ich zapneš",
"notifyUpstreamLabel": "Upozorniť ma na novú dostupnú verziu",
"notifyUpstreamHelp": "Odošle upozornenie app_update_available do kanálov zapnutých v Nastaveniach → Upozornenia. Vypni, ak túto aplikáciu na svojom serveri nemôžeš aktualizovať.",
"excludeFromBadgeLabel": "Vylúčiť z počítadla aktualizácií LXC",
"excludeFromBadgeHelp": "Nezapočítajte túto aplikáciu do odznaku súhrnných aktualizácií na karte zoznamu LXC.Užitočné, keď ste úmyselne pripnutý ku konkrétnej verzii (požiadavka na sledovanie, zmrazenie kompatibility).Nemá vplyv na vlastný stav karty Aplikácia ani na odchádzajúce upozornenie.",
"excludeFromBadgeHelp": "Nezapočíta túto aplikáciu do počtu aktualizácií v zozname LXC. Hodí sa, ak chceš zámerne zostať pri konkrétnej verzii, napríklad kvôli kompatibilite. Stav na záložke Aplikácie aj upozornenia zostanú nezmenené.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"stackMemberTitle": "Časť aplikácie s viacerými kontajnermi",
"stackMemberBody": "Tento kontajner podporuje aplikáciu, ktorej hlavný kontajner je {primary}. Zobrazuje sa tam aplikácia, jej webové odkazy a jej verzia."
@@ -1872,7 +1995,7 @@
},
"networkInterfaces": {
"title": "Vylúčené sieťové rozhrania",
"description": "Vylúčte sieťové rozhrania, napríklad most, bond alebo fyzické sieťové karty, z kontroly stavu a upozornení. Hodí sa to pre rozhrania, ktoré sú zámerne vypnuté alebo sa nepoužívajú.",
"description": "Vylúčte sieťové rozhrania, napríklad sieťové mosty (bridge), bond alebo fyzické sieťové karty, z kontroly stavu a upozornení. Hodí sa to pre rozhrania, ktoré sú zámerne vypnuté alebo sa nepoužívajú.",
"emptyTitle": "Nenašli sa žiadne sieťové rozhrania",
"interface": "Rozhranie",
"down": "VYPNUTÉ",
@@ -2295,6 +2418,42 @@
"updateManyDescription": "Počet funkcií, ktoré sa postupne spustia: {count}.",
"legacySourceTitle": "Staršia optimalizácia - kliknutím zobrazíte zdroj",
"sourceTitle": "Kliknutím zobrazíte zdrojový kód",
"sourceDialogTitle": "Zdrojový kód: {name}",
"sourceLoading": "Načítava sa zdrojový kód…",
"sourceUnavailable": "Zdrojový kód nie je dostupný.",
"sourceLoadFailed": "Zdrojový kód sa nepodarilo načítať. Skús to znova.",
"sourceFunctionMissing": "Funkcia sa nenašla v dostupných skriptoch.",
"sourceUnknownTool": "Neznámy nástroj: {tool}",
"sourceNoMapping": "K nástroju {tool} nie je priradená funkcia.",
"sourceToolNames": {
"subscription_banner": "Odstránenie upozornenia na predplatné",
"time_sync": "Synchronizácia času",
"apt_languages": "Vynechanie jazykových balíkov APT",
"journald": "Optimalizácia journald",
"logrotate": "Optimalizácia logrotate",
"system_limits": "Zvýšenie systémových limitov",
"memory_settings": "Optimalizácia nastavení pamäte",
"zram_swap": "Profil swapu ZRAM",
"kernel_panic": "Nastavenie správania pri páde jadra",
"apt_ipv4": "Vynútenie IPv4 pre APT",
"kexec": "Rýchly reštart cez kexec",
"rpc": "Vypnutie RPC / rpcbind",
"motd": "Vlastná uvítacia správa MOTD",
"system_utils": "Systémové nástroje",
"network_optimization": "Optimalizácia siete",
"bashrc_custom": "Prispôsobenie bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (ochrana SSD)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Opravy pre procesory AMD (Ryzen/EPYC)",
"persistent_network": "Trvalé názvy sieťových rozhraní",
"vfio_iommu": "Priame priradenie zariadení (VFIO/IOMMU)",
"lvm_repair": "Oprava hlavičiek fyzických zväzkov LVM",
"repo_cleanup": "Vyčistenie repozitárov",
"proxmox_repos": "Repozitáre APT pre Proxmox",
"entropy": "Generovanie entropie (haveged)"
},
"legacy": "staršie",
"updateToolTitle": "Aktualizovať {name} na v{version}",
"availableUpdates": "Dostupné aktualizácie",
@@ -3572,7 +3731,7 @@
},
"loading": {
"title": "Načítavam údaje o sieti...",
"description": "Kontrolujem rozhrania, sieťové mosty a premávku"
"description": "Kontrolujem rozhrania, sieťové mosty (bridge) a premávku"
},
"errors": {
"serverUnavailableTitle": "Server nie je dostupný",
@@ -3586,7 +3745,7 @@
},
"sections": {
"physicalInterfaces": "Fyzické rozhrania",
"bridgeInterfaces": "Sieťové mosty",
"bridgeInterfaces": "Sieťové mosty (bridge)",
"vmLxcInterfaces": "Sieťové rozhrania VM a LXC"
},
"summary": {
@@ -3606,8 +3765,8 @@
},
"interfaceTypes": {
"physical": "Fyzické",
"bridge": "Most",
"bridges": "Mosty",
"bridge": "Sieťový most (bridge)",
"bridges": "Sieťové mosty (bridge)",
"bond": "Bond",
"vlan": "VLAN",
"virtual": "Virtuálne",
@@ -3701,7 +3860,7 @@
"inactiveTitle": "Rozhranie je neaktívne",
"inactiveDescription": "Toto rozhranie je momentálne vypnuté. Štatistiky sieťovej premávky nie sú dostupné.",
"bondConfiguration": "Nastavenie bondu",
"bridgeConfiguration": "Nastavenie mosta"
"bridgeConfiguration": "Nastavenie sieťového mosta (bridge)"
},
"flow": {
"title": "Tok siete"
@@ -4246,7 +4405,7 @@
"unmountTitle": "Odpojiť tento cieľ",
"whereIsBorgRepo": "Kde je Borg repozitár?",
"backupsKeptMessage": "Existujúce zálohy sa neodstránia.",
"localAttachHelpMessage": "Najprv pripojte jednotky USB.Ak chcete použiť cestu k úložisku PVE + /dump, nechajte pole prázdne (záložný: /var/lib/vz/dump).",
"localAttachHelpMessage": "Najprv pripoj USB disk. Ak pole necháš prázdne, použije sa cesta úložiska PVE doplnená o /dump; náhradná cesta je /var/lib/vz/dump.",
"sshAuthorizedKeyHelp": "Pred pridaním do ~/.ssh/authorized_keys pre {user} na vzdialenom hostiteľovi skontrolujte riadok nižšie.",
"sshKeyHelpMessage": "Cesta súkromného kľúča na tomto hostiteľovi.Použite {action}, potom skontrolujte a pridajte zobrazený riadok verejného kľúča do ~/.ssh/authorized_keys vzdialeného používateľa.",
"sshKeySetupHelp": "Vytvorí kľúč na zadanej lokálnej ceste, ak žiadny neexistuje;inak prečíta svoj verejný kľúč.Na vzdialenom hostiteľovi nie je nainštalovaný žiadny kľúč.",
@@ -4290,6 +4449,124 @@
"usesSharedKeyfileAt": "Používa spoločný súbor s kľúčom v",
"pbsHelp": "Monitor používa {keyfile} s {option} na šifrované zálohy a obnovy PBS.Súbor kľúča je uložený v režime {mode}.Uschovajte si bezpečnú kópiu mimo tohto hostiteľa."
},
"apiErrors": {
"requestFailed": "Požiadavka zlyhala.",
"absolutePath": "Cesta musí začínať znakom /.",
"pathConfigured": "Táto cesta už je nastavená.",
"pathRequired": "Zadaj cestu.",
"pathQuery": "V požiadavke chýba cesta.",
"pathNotFound": "Cesta sa nenašla.",
"invalidJobId": "Neplatný identifikátor úlohy.",
"jobNotFound": "Úloha sa nenašla.",
"runnerMissing": "Na serveri chýba skript na spustenie zálohy.",
"noRestore": "Nie je tu žiadny záznam obnovy na zatvorenie.",
"invalidFile": "Neplatný názov súboru.",
"notFound": "Požadovaný záznam sa nenašiel.",
"logOutside": "Cesta k záznamu je mimo povoleného priečinka.",
"targetsList": "Ciele musia byť zadané ako zoznam.",
"emptyExpression": "Zadaj časový plán.",
"invalidId": "V identifikátore použi iba písmená, číslice, _ alebo -.",
"backend": "Vyber cieľ PBS, miestny archív alebo Borg.",
"remoteBackend": "Vyber cieľ PBS alebo Borg.",
"remoteBackendQuoted": "Vyber cieľ PBS alebo Borg.",
"profileMode": "Vyber predvolený alebo vlastný profil.",
"borgAttached": "Borg sa nedá pripojiť k úlohe PVE; vzdump zapisuje iba na PBS alebo miestne úložisko.",
"borgAttachedLong": "Borg sa nedá pripojiť k úlohe PVE; vzdump zapisuje iba na PBS alebo miestne úložisko.",
"parentRequired": "Vyber nadradenú úlohu PVE aj jej úložisko.",
"absolutePaths": "Zadaj zoznam ciest, ktoré začínajú znakom /.",
"customPath": "Do vlastného profilu pridaj aspoň jednu cestu.",
"defaultPaths": "Cesty predvoleného profilu sa nepodarilo zistiť.",
"retentionObject": "Pravidlá uchovávania záloh majú neplatný formát.",
"pbsRepository": "Zadaj repozitár PBS.",
"pbsEncryption": "Vyber režim šifrovania PBS: bez šifrovania, nový kľúč alebo existujúci kľúč.",
"pbsKeyFailed": "Súbor so šifrovacím kľúčom PBS sa nepodarilo vytvoriť.",
"pbsClientMissing": "Súbor so šifrovacím kľúčom PBS sa nepodarilo vytvoriť. Skontroluj, či je nainštalovaný proxmox-backup-client.",
"borgRepository": "Zadaj repozitár Borg.",
"borgEncryption": "Vyber podporovaný režim šifrovania Borg: none, repokey, keyfile alebo authenticated.",
"encryption": "Vyber podporovaný režim šifrovania: none, repokey, keyfile alebo authenticated.",
"borgPassphrase": "Šifrovaný repozitár Borg potrebuje heslo. Ulož ho pri cieli alebo ho zadaj v požiadavke.",
"passphrase": "Pri zapnutom šifrovaní zadaj heslo.",
"password": "Zadaj heslo.",
"deviceConfirmation": "Na potvrdenie zadaj presný názov zariadenia.",
"invalidDevice": "Neplatná cesta k zariadeniu.",
"absoluteMountpoint": "Prípojný bod musí byť cesta začínajúca znakom /.",
"archiveLayout": "Archív nemá očakávanú štruktúru: chýba priečinok rootfs/.",
"archiveMissing": "Archív sa nenašiel.",
"archiveNotFile": "Cesta k archívu neoznačuje bežný súbor.",
"invalidName": "Neplatný názov.",
"invalidStaging": "Neplatná cesta k dočasným súborom.",
"jobRename": "Názov existujúcej úlohy sa nedá zmeniť. Ak ju chceš premenovať, odstráň ju a vytvor novú.",
"absoluteKey": "Cesta k súboru s kľúčom musí začínať znakom /.",
"mode": "Vyber miestny cieľ alebo pripojenie cez SSH.",
"nameRequired": "Zadaj názov; použi iba písmená, číslice, _ alebo -.",
"noLog": "Pre túto úlohu zatiaľ nie je dostupný záznam.",
"noPbsConfig": "Súbor s ručne nastavenými cieľmi PBS sa nenašiel.",
"outputMissing": "Chýba výstupný súbor.",
"parserJson": "Výstup spracovania archívu nemá platný formát JSON.",
"restoreJson": "Výstup prípravy obnovy nemá platný formát JSON.",
"repoRequired": "Zadaj repozitár.",
"repoSnapshot": "Vyber repozitár aj zálohu.",
"borgRepoSnapshot": "Pre Borg vyber repozitár aj zálohu.",
"pbsRepoSnapshot": "Pre PBS vyber repozitár aj zálohu.",
"repoName": "Zadaj názov repozitára.",
"restoreMissing": "Na tomto serveri chýbajú nástroje na obnovu.",
"serverDatastore": "Zadaj server aj dátové úložisko.",
"snapshotRequired": "Vyber zálohu.",
"source": "Vyber zdroj PBS, Borg alebo miestny archív.",
"sshPort": "Port SSH musí byť celé číslo od 1 do 65535.",
"sshRequired": "Pre SSH zadaj používateľa, hostiteľa a vzdialenú cestu.",
"repositoryPath": "Cieľ nemá nastavenú cestu k repozitáru.",
"taskExpired": "Úloha sa nenašla alebo už vypršala.",
"invalidExpression": "Neplatný časový plán.",
"noReport": "Kontrola nevrátila žiadnu správu.",
"parseManifestFailed": "Spracovanie manifestu skončilo chybou.",
"jobExists": "Úloha s názvom {value} už existuje.",
"pathMissing": "Na tomto serveri sa nenašla cesta: {value}",
"deviceMissing": "Zariadenie neexistuje: {value}",
"mountpoint": "Táto cesta nie je prípojný bod: {value}",
"localArchiveMissing": "Miestny archív sa nenašiel: {value}",
"borgDestination": "Cieľ Borg „{value}“ sa nenašiel.",
"pbsDestination": "Cieľ PBS „{value}“ sa nenašiel.",
"pbsPassword": "Pre repozitár PBS „{value}“ nie je dostupné heslo.",
"borgRepoMissing": "Repozitár Borg „{value}“ nie je nastavený.",
"pbsRepoMissing": "Repozitár PBS „{value}“ nie je nastavený.",
"borgTargetMissing": "Cieľ Borg sa nenašiel: {value}",
"pbsTargetMissing": "Repozitár PBS sa nenašiel: {value}",
"pveJobMissing": "Úloha zálohovania PVE sa nenašla: {value}",
"statePersist": "Stav sa nepodarilo uložiť: {value}",
"sshDirectory": "Priečinok SSH sa nepodarilo pripraviť: {value}",
"logRead": "Záznam sa nepodarilo prečítať: {value}",
"envUpdate": "Súbor .env sa nepodarilo aktualizovať: {value}",
"pbsConfigWrite": "Nastavenia PBS sa nepodarilo uložiť: {value}",
"persist": "Údaje sa nepodarilo uložiť: {value}",
"archiveRemove": "Archív sa nepodarilo odstrániť: {value}",
"start": "Úlohu sa nepodarilo spustiť: {value}",
"borgTargetsUpdate": "Súbor borg-targets.txt sa nepodarilo aktualizovať: {value}",
"pbsTargetsUpdate": "Súbor pbs-manual-configs.txt sa nepodarilo aktualizovať: {value}",
"unitsUpdate": "Jednotky systemd sa nepodarilo aktualizovať: {value}",
"borgTargetsWrite": "Súbor borg-targets.txt sa nepodarilo uložiť: {value}",
"jobWrite": "Súbory úlohy sa nepodarilo uložiť: {value}",
"localTargetWrite": "Súbor local-target.conf sa nepodarilo uložiť: {value}",
"unitsWrite": "Jednotky systemd sa nepodarilo uložiť: {value}",
"format": "Formátovanie zlyhalo: {value}",
"calendar": "Neplatný časový plán: {value}",
"mount": "Disk sa nepodarilo pripojiť: {value}",
"parserInvocation": "Spracovanie archívu sa nepodarilo spustiť: {value}",
"preflightInvocation": "Kontrolu pred obnovou sa nepodarilo spustiť: {value}",
"publicKeyMissing": "Po vytvorení kľúča sa nenašiel verejný kľúč: {value}",
"retentionInteger": "Pravidlo uchovávania „{value}“ musí byť celé číslo.",
"retentionNegative": "Pravidlo uchovávania „{value}“ nesmie byť záporné.",
"keygen": "Vytvorenie kľúča SSH zlyhalo: {value}",
"systemctl": "Správa služby cez systemctl zlyhala: {value}",
"unmount": "Disk sa nepodarilo odpojiť: {value}",
"taskIncomplete": "Úloha ešte nie je dokončená. Jej stav: {value}",
"unknownKind": "Neznámy typ: {value}",
"unknownMode": "Neznámy režim: {value}",
"defaultRemove": "Cesta {value} je predvolená a nedá sa odstrániť.",
"defaultAdd": "Cesta {value} je predvolená; nemusíš ju pridávať.",
"keyImport": "V {value} nie je uložený súbor s kľúčom PBS. Najprv ho importuj.",
"storageMismatch": "Úložisko sa nezhoduje: úloha PVE používa {value}, požiadavka uvádza {other}."
},
"errors": {
"backendNoStagingPath": "Cieľ nevrátil cestu k dočasným súborom.",
"deleteFailed": "Mazanie zlyhalo: {error}",
@@ -4875,7 +5152,7 @@
"keepAll": "Ponechať všetky zálohy",
"description": "Predvolená politika uchovávania záloh uložená v Proxmoxe. Zálohovacie úlohy s vlastnou politikou majú pred týmto predvoleným nastavením prednosť.",
"refresh": "Obnoviť",
"saved": "Politika uchovávania záloh úložiska bola uložená. Prerezávanie sa nevykonalo.",
"saved": "Pravidlá uchovávania záloh na úložisku boli uložené. Žiadne zálohy sa teraz neodstránili.",
"choosePolicy": "Vyberte Ponechať všetky alebo aspoň jeden počet uchovávaných záloh väčší ako nula.",
"saveFailed": "Nepodarilo sa uložiť politiku uchovávania záloh úložiska. Ak sa konfigurácia zmenila, obnovte zoznam.",
"loadFailed": "Nepodarilo sa načítať politiku uchovávania záloh úložiska.",
@@ -5387,6 +5664,7 @@
"websocketError": "Chyba WebSocket spojenia",
"connectionClosed": "Spojenie bolo zatvorené",
"completedSuccessfully": "Skript sa úspešne dokončil",
"cancelled": "Operácia bola zrušená.",
"completedWithError": "Skript skončil s chybovým kódom {code}",
"yes": "Áno",
"yourInput": "Tvoj vstup:",
@@ -5709,7 +5987,7 @@
"nextStep": {
"missing": "Nastavte si držanie na týchto pracovných miestach, alebo na sklade, na ktorý píšu. Proxmox berie nastavenie úlohy ako prvý, potom úložisko je, potom uzol predvolený.",
"notDeclared": "Tieto práce uchovávajú všetky kópie, ktoré robia. To je zámerná voľba pre niektoré destinácie, ale potvrďte, že cieľ má priestor na rast.",
"onServer": "Tieto úlohy píšu na záložný server, ktorý ich prerezáva podľa vlastných pravidiel. Z tohto uzla sa nedá čítať to, čo drží, takže skontrolujte, či tam nie je."
"onServer": "Tieto úlohy ukladajú zálohy na zálohovací server. Ten odstraňuje staré zálohy podľa vlastných pravidiel. Z tohto uzla sa nedá zistiť, ktoré zálohy uchováva; skontroluj ich priamo na zálohovacom serveri."
},
"explanation": "O tom, koľko výtlačkov sa uchováva a ako dlho sa uchováva. Bez nej sa cieľ naplní, kým neprestane prijímať nové kópie, a záloha, ktorú nemožno napísať, je tá, o ktorej sa dozviete v deň, keď ju potrebujete."
},
@@ -6119,11 +6397,11 @@
}
},
"bridge_without_ports": {
"title": "Porty bridgeov",
"rationale": "Konfigurácia portov každého bridge. Bridge bez fyzického portu obsluhuje internú alebo smerovanú sieť.",
"title": "Porty sieťových mostov (bridge)",
"rationale": "Konfigurácia portov každého sieťového mosta (bridge). Sieťový most (bridge) bez fyzického portu obsluhuje internú alebo smerovanú sieť.",
"summary": {
"allConnected": "Všetkých {total} bridgeov má port",
"isolated": "{count} z {total} bridgeov nemá port",
"allConnected": "Všetkých {total} sieťových mostov (bridge) má port",
"isolated": "{count} z {total} sieťových mostov (bridge) nemá port",
"evaluationFailed": "Kontrolu sa nepodarilo vyhodnotiť"
}
}
@@ -6339,13 +6617,13 @@
"network": "Sieť",
"adapters": "Adaptéry",
"bond": "Bond",
"bridge": "Bridge",
"bridges": "Bridge",
"bridge": "Sieťový most (bridge)",
"bridges": "Sieťové mosty (bridge)",
"physicalAdapters": "Fyzické adaptéry",
"interface": "Rozhranie",
"driver": "Ovládač",
"state": "Stav",
"networkDiagramNote": "Cesta od kábla ku každému VM a LXC: fyzický adaptér, bond, ak ich zoskupuje, bridge a pripojené VM a LXC.",
"networkDiagramNote": "Cesta od kábla ku každému VM a LXC: fyzický adaptér, bond, ak ich zoskupuje, sieťový most (bridge) a pripojené VM a LXC.",
"storageAndProtection": "Úložisko a ochrana",
"storage": "Úložisko",
"content": "Obsah",
@@ -7367,35 +7645,40 @@
"title": "Proxmox Backup Server",
"server": "Server PBS",
"chooseServer": "Vybrať server PBS",
"local": "Miestny PBS",
"local": "Lokálny PBS",
"thisNode": "PBS na tomto uzle",
"updatedAt": "Aktualizované o {time}",
"servicesActive": "Služby sú aktívne",
"detectedTitle": "PBS zistené v úložisku Proxmox",
"detectedHelp": "Tieto Proxmox Backup Servery sú nastavené ako úložisko záloh. Pripojte každý z nich API tokenom len na čítanie, aby ste tu videli jeho datastory, úlohy a joby.",
"detectedTitle": "PBS zistené v nastaveniach úložísk Proxmoxu",
"detectedHelp": "Tieto servery Proxmox Backup Server sú nastavené ako úložiská záloh. Pripoj ich pomocou API tokenu iba na čítanie a zobrazia sa tu ich dátové úložiská, úlohy a plány údržby.",
"connect": "Pripojiť",
"tasksTab": "Úlohy",
"signInWith": "Prihlásiť sa pomocou",
"methodPassword": "Používateľ a heslo",
"methodToken": "API token",
"username": "Používateľ (pouzivatel@realm)",
"username": "Používateľ",
"password": "Heslo",
"passwordHelp": "Heslo sa použije iba raz na vytvorenie API tokenu len na čítanie (rola Audit) pre tento Monitor. Uloží sa iba tento token, nikdy nie heslo. Najprv zapnite overovanie v Monitore.",
"passwordHelp": "Heslo sa použije iba raz na vytvorenie API tokenu iba na čítanie s rolou Audit pre tento Monitor. Uloží sa iba token, heslo sa neukladá. Najprv zapni overovanie v Monitore.",
"cancel": "Zrušiť",
"refresh": "Obnoviť",
"manage": "Spravovať pripojenia PBS",
"loadFailed": "Nepodarilo sa načítať stav PBS.",
"loading": "Načítavajú sa údaje PBS…",
"noServer": "Pridajte pripojenie PBS na zobrazenie dátových úložísk, úloh a plánov.",
"permissionsHelp": "Pred uložením prihlasovacích údajov povoľte overovanie v Monitore. Použite vyhradený API token PBS s rolou Audit na / v ACL používateľa aj tokenu, so zapnutým dedením. Tento panel údaje PBS iba číta.",
"noServer": "Pridaj pripojenie PBS a zobrazia sa jeho dátové úložiská, úlohy a plány údržby.",
"permissionsHelp": "Pred uložením prihlasovacích údajov zapni overovanie v Monitore. Použi vyhradený API token PBS. Používateľ aj token musia mať v zozname prístupových oprávnení (ACL) rolu Audit na ceste / so zapnutým dedením. Tento panel údaje PBS iba číta.",
"editConnection": "Pripojenie na úpravu",
"add": "Pridať nové pripojenie",
"name": "Názov",
"url": "HTTPS URL (pre pôvodný port PBS uveďte :8007)",
"token_id": "ID API tokenu (user@realm!token)",
"token_secret": "Tajný kľúč API tokenu (ponechajte prázdne, ak chcete zachovať uložený kľúč)",
"fingerprint": "Odtlačok SHA256 certifikátu (voliteľný)",
"tlsHelp": "Pre bežné overenie certifikačnej autority a názvu hostiteľa ponechajte odtlačok prázdny. Pri certifikáte s vlastným podpisom skopírujte jeho odtlačok SHA256 z konzoly PBS alebo iného dôveryhodného kanála. Odtlačok sa kontroluje pred odoslaním prihlasovacích údajov.",
"url": "Adresa HTTPS",
"urlHelp": "Štandardný port PBS je 8007, napríklad https://pbs.example:8007.",
"usernameHelp": "Použi meno a overovaciu oblasť oddelené znakom @, napríklad root@pam.",
"tokenIdHelp": "Použi formát meno@oblasť!token, napríklad monitor@pbs!proxmenux.",
"tokenSecretHelp": "Zadaj tajný kľúč k tomuto API tokenu.",
"tokenSecretKeepHelp": "Prázdne pole zachová uložený tajný kľúč tokenu.",
"token_id": "ID API tokenu",
"token_secret": "Tajný kľúč API tokenu",
"fingerprint": "Odtlačok SHA256 (voliteľný)",
"tlsHelp": "Pri bežnom overovaní certifikačnej autority a názvu hostiteľa nechaj odtlačok prázdny. Pri certifikáte s vlastným podpisom skopíruj odtlačok SHA256 z konzoly PBS alebo iného dôveryhodného zdroja. Kontroluje sa ešte pred odoslaním prihlasovacích údajov.",
"saved": "Pripojenie PBS bolo overené a uložené.",
"saveFailed": "Nepodarilo sa uložiť pripojenie PBS.",
"testing": "Overuje sa pripojenie…",
@@ -7407,9 +7690,9 @@
"total": "Kapacita súborového systému",
"used": "Použité",
"free": "Dostupné",
"noDatastores": "Žiadne dostupné dátové úložiská. Skontrolujte konfiguráciu úložísk a oprávnenia používateľa a tokenu.",
"noDatastores": "Nie sú dostupné žiadne dátové úložiská. Skontroluj ich nastavenia a oprávnenia používateľa aj tokenu.",
"tasks": "Nedávne úlohy",
"noTasks": "Neboli vrátené žiadne nedávne úlohy.",
"noTasks": "PBS nevrátil žiadne nedávne úlohy.",
"task": "Úloha",
"started": "Spustené",
"status": "Stav",
@@ -7419,7 +7702,7 @@
"schedule": "Plán",
"sync": "Synchronizácia",
"verify": "Overenie",
"prune": "Prerezávanie",
"prune": "Odstraňovanie starých záloh",
"overview": "Prehľad",
"logs": "Systémové záznamy",
"terminal": "Terminál",
@@ -7430,52 +7713,111 @@
"memory": "Pamäť",
"uptime": "Čas prevádzky",
"capacityUnavailable": "Kapacita nie je k dispozícii.",
"datastoresUnavailable": "{count} nie je k dispozícii",
"datastoresUnavailable": "Nedostupné úložiská: {count}",
"datastoresOnline": "Všetky dostupné",
"moreDatastores": "+{count} ďalších dátových úložísk",
"estimatedFull": "Odhadovaný plný",
"estimatedFullUnknown": "Zatiaľ nie je dostatok údajov",
"estimatedFull": "Odhadované zaplnenie",
"estimatedFullUnknown": "Zatiaľ sa nedá odhadnúť",
"duration": "Trvanie",
"noJobs": "Nie sú nakonfigurované žiadne plány synchronizácie, overovania ani odstraňovania."
"noJobs": "Nie sú nastavené žiadne plány synchronizácie, overovania ani odstraňovania starých záloh.",
"removeFailed": "Nepodarilo sa odstrániť pripojenie PBS.",
"savedRefreshFailed": "Pripojenie PBS sa uložilo, ale nepodarilo sa obnoviť zoznam pripojení. Pred ďalšou zmenou obnov stránku.",
"removedRefreshFailed": "Pripojenie PBS sa odstránilo, ale nepodarilo sa obnoviť zoznam pripojení. Obnov stránku.",
"technicalDetails": "Technické podrobnosti",
"datastoreType": "Dátové úložisko",
"errors": {
"configRead": "Nepodarilo sa načítať uložené pripojenia PBS. Skontroluj konfiguračný súbor a oprávnenia.",
"invalidConfig": "Nastavenia pripojenia PBS nie sú platné. Skontroluj formulár a skús to znova.",
"invalidField": "Niektoré pole pripojenia nie je platné alebo je príliš dlhé.",
"invalidUrl": "Použi adresu HTTPS bez vložených prihlasovacích údajov, parametrov za otáznikom a časti za znakom #.",
"nameRequired": "Zadaj názov servera s najviac 128 znakmi.",
"tokenIdFormat": "Zadaj ID API tokenu PBS vo formáte user@realm!token.",
"tokenSecret": "Zadaj platný tajný kľúč API tokenu bez medzier a riadiacich znakov.",
"invalidTokenId": "ID API tokenu môže obsahovať iba znaky ASCII.",
"invalidFingerprint": "Zadaj odtlačok SHA256 certifikátu: 64 šestnástkových znakov, prípadne oddelených dvojbodkami.",
"fingerprintMismatch": "Certifikát PBS sa nezhoduje s odtlačkom. Prihlasovacie údaje sa neodoslali.",
"tokenRejected": "PBS odmietol API token alebo jeho oprávnenia. Skontroluj zoznam prístupových oprávnení (ACL) používateľa aj tokenu.",
"invalidResponse": "PBS vrátil neočakávanú odpoveď. Presmerovania sa nenasledujú.",
"invalidJson": "PBS vrátil neplatný dokument JSON.",
"connectFailed": "Nepodarilo sa pripojiť k PBS cez overené HTTPS. Skontroluj adresu, certifikát a službu.",
"invalidId": "ID pripojenia PBS nie je platné. Obnov zoznam pripojení.",
"invalidDatastores": "PBS vrátil neplatný zoznam dátových úložísk.",
"connectionLimit": "Môžeš nastaviť najviac {count} pripojení PBS.",
"username": "Zadaj používateľa PBS vo formáte user@realm, napríklad root@pam.",
"password": "Zadaj heslo používateľa PBS.",
"loginRejected": "PBS odmietol používateľské meno alebo heslo.",
"invalidLogin": "PBS vrátil neplatnú odpoveď na prihlásenie.",
"tokenCreateDenied": "Tento používateľ PBS nemôže vytvárať API tokeny.",
"invalidToken": "PBS vrátil neplatný API token.",
"auditRoleDenied": "Tento používateľ PBS nemôže prideliť rolu Audit. Prihlás sa ako root@pam alebo iný správca.",
"notFound": "Pripojenie PBS sa nenašlo. Obnov zoznam pripojení.",
"readFailed": "Nepodarilo sa načítať údaje PBS. Skontroluj službu a oprávnenia.",
"invalidList": "PBS vrátil neplatný zoznam údajov.",
"localUnavailable": "PBS na tomto hostiteľovi nie je dostupný.",
"authRequired": "Pred správou vzdialených pripojení PBS zapni lokálne overovanie v Monitore.",
"datastoreOffline": "Dátové úložisko je odpojené, v režime údržby alebo jeho zariadenie nie je pripojené.",
"datastoreUnavailable": "Súborový systém dátového úložiska nie je dostupný."
},
"taskTypes": {
"backup": "Zálohovanie", "restore": "Obnova", "verify": "Overovanie",
"verifyGroup": "Overovanie skupiny záloh", "verifySnapshot": "Overovanie snímky",
"prune": "Odstraňovanie starých záloh", "garbageCollection": "Čistenie nepoužívaných dát", "sync": "Synchronizácia",
"updateCheck": "Kontrola aktualizácií", "terminal": "Terminál", "createDatastore": "Vytvorenie dátového úložiska",
"other": "Iná úloha ({type})"
},
"states": { "ok": "V poriadku", "unknown": "Neznámy", "warnings": "Upozornenia: {count}", "warning": "Upozornenie", "error": "Chyba" },
"serviceStates": { "active": "Aktívna", "inactive": "Neaktívna", "failed": "Zlyhala", "activating": "Spúšťa sa", "deactivating": "Zastavuje sa", "unknown": "Neznámy stav" },
"durationUnits": { "seconds": "{count} s", "minutes": "{count} min", "hours": "{count} h", "days": "{count} d" }
},
"monitorLinks": {
"groups": {
"cluster": "Cluster",
"cluster": "Klaster",
"standalone": "Samostatné uzly",
"pbs": "Proxmox Backup Server"
},
"thisNode": "Tento uzol",
"checking": "Kontrolujú sa uzly…",
"loadFailed": "Nepodarilo sa načítať zoznam uzlov.",
"empty": "Zatiaľ žiadne ďalšie uzly. Pridajte jeden, aby ste odtiaľto otvorili jeho Monitor.",
"empty": "Zatiaľ tu nie sú ďalšie uzly. Pridaj uzol a jeho Monitor otvoríš priamo odtiaľto.",
"noResponse": "Žiadna odpoveď",
"pbsInterface": "rozhranie PBS",
"openNewTab": "Otvoriť na novej karte",
"manage": "Pridať alebo spravovať uzly…",
"dialogTitle": "Prepojené uzly",
"dialogDescription": "Uzly klastra a nakonfigurované Proxmox Backup Server sa zisťujú automaticky. Každý Monitor má vlastné prihlásenie.",
"dialogDescription": "Uzly klastra a nastavené servery Proxmox Backup Server sa zisťujú automaticky. Do Monitora každého uzla sa prihlasuješ samostatne.",
"addTitle": "Pridať uzol",
"name": "Meno",
"address": "Adresa monitora",
"name": "Názov uzla",
"address": "Adresa Monitora",
"type": "Typ",
"add": "Pridať",
"save": "Uložiť",
"remove": "Odstrániť",
"visible": "Viditeľné",
"visible": "Zobraziť",
"autoAddress": "Automaticky",
"detected": "Zistené",
"added": "Pridané",
"detected": "Zistený",
"added": "Pridaný",
"saveFailed": "Nepodarilo sa uložiť zoznam uzlov.",
"backTo": "Späť na {name}",
"cameFromTitle": "Pridať uzol monitora?",
"cameFromBody": "Pridajte {name} do prepojených uzlov tohto monitora.",
"cameFromTitle": "Pridať uzol Monitora?",
"cameFromBody": "Pridaj {name} medzi prepojené uzly tohto Monitora.",
"notNow": "Teraz nie",
"knownTitle": "Pridať prepojené uzly?",
"knownBody": "{name} prepája tieto monitory. Vyberte tie, ktoré chcete pridať do tohto monitora.",
"knownBody": "Uzol {name} má prepojené tieto uzly. Vyber tie, ktoré chceš pridať aj sem.",
"addSelected": "Pridať vybrané",
"pbsWebInterface": "Webové rozhranie PBS",
"changeAddress": "Zmeniť adresu",
"cancel": "Zrušiť"
"cancel": "Zrušiť",
"errors": {
"addressRequired": "Zadaj adresu Monitora.",
"invalidAddress": "Adresa musí začínať na http:// alebo https:// a obsahovať názov hostiteľa alebo IP adresu.",
"addressCredentials": "Adresa nesmie obsahovať používateľské meno ani heslo.",
"invalidPort": "Port v adrese nie je platný. Zadaj číslo od 1 do 65535.",
"invalidRequest": "Požiadavka nie je platná. Obnov stránku a skús to znova.",
"limit": "Môžeš pridať najviac {count} uzlov.",
"duplicate": "Táto adresa už je v zozname.",
"notFound": "Uzol sa nenašiel. Obnov zoznam a skús to znova.",
"detectedRemove": "Odstrániť môžeš iba ručne pridané uzly. Automaticky zistené uzly môžeš skryť."
}
},
"apiErrors": {
"http": "Požiadavka na API zlyhala: {status} {statusText}",
+375 -8
View File
@@ -1231,7 +1231,9 @@
"update": "Uppdatera",
"modify": "Ändra",
"recover": "Återställ",
"registerRestored": "Registrera igen",
"terminalTitleUpdate": "Uppdatera OCI-applikation",
"terminalTitleRecoverPrevious": "Återställ den tidigare OCI-installationen",
"terminalTitleModify": "Ändra OCI-applikation",
"terminalTitleRecover": "Återställ återställd OCI-applikation",
"terminalDescription": "Samma process körs som i ProxMenux-menyn: OCI manager Apps → Hantera installerade OCI-applikationer.",
@@ -1760,7 +1762,128 @@
"dockerEvidenceImage": "Avbild",
"dockerEvidencePort": "Publicerad TCP-port",
"dockerEvidenceProject": "Compose-projekt",
"dockerEndpointReview": "Container med publicerade TCP-portar. Bekräfta namnet och webbadressen innan du sparar. Registreringen konfigurerar inga uppdateringar."
"dockerEndpointReview": "Container med publicerade TCP-portar. Bekräfta namnet och webbadressen innan du sparar. Registreringen konfigurerar inga uppdateringar.",
"confirmRemove": "Ta bort applikationens post från fliken App? Detta avinstallerar inte applikationen.",
"packageExample": "t.ex. {package}",
"packageHelpBefore": "Fylls i automatiskt från Namn. Kontrollera med",
"packageHelpAfter": "i CT:n.",
"binaryHelpBefore": "Absolut sökväg. Hitta den med",
"binaryHelpOr": "eller",
"errors": {
"ociBusy": "en OCI-åtgärd pågår på den här containern",
"dpkgMissing": "{package} är inte installerat via dpkg",
"dpkgFailed": "dpkg-query misslyckades",
"dpkgNoVersion": "ingen version matchade i utdata från dpkg",
"apkFailed": "apk info misslyckades",
"apkNoVersion": "ingen version matchade i utdata från apk",
"fileRead": "kunde inte läsa {path}",
"fileNoMatch": "file_regex matchade inte {path}",
"binaryFailed": "anropet av binärfilen misslyckades",
"binaryNoVersion": "ingen version matchade i utdata från binärfilen",
"pythonFailed": "python -c importlib.metadata misslyckades",
"pythonNoVersion": "ingen version matchade i utdata från Python-distributionen",
"dockerImageResolve": "kunde inte lösa containerns installerade avbild",
"dockerInspectFailed": "docker inspect misslyckades",
"dockerLabelMissing": "containern saknar etiketten {label}",
"dockerLabelMutable": "docker-etiketten rapporterar {tag} (föränderlig tagg, inte en version)",
"dockerLabelNoVersion": "ingen version matchade i utdata från docker-etiketten",
"dockerExecFailed": "docker exec misslyckades",
"dockerExecNoVersion": "ingen version matchade i utdata från docker exec",
"commandEmpty": "command_argv är tom",
"commandFailed": "kommandot misslyckades",
"commandNoVersion": "ingen version matchade i kommandots utdata",
"unsupportedMethod": "metod som inte stöds: {method}",
"unknownUpstream": "okänd upstream_type: {type}",
"githubNotFound": "{repo}: hittades inte",
"githubRejected": "github avvisade förfrågan (403)",
"githubError": "github-fel {code}",
"noRelease": "inget tagg- eller releasenamn i svaret",
"tagNoMatch": "tag_regex matchade inte '{tag}'",
"httpError": "http-fel {code}",
"invalidJson": "ogiltigt JSON-svar: {detail}",
"jsonPathUnresolved": "json_path '{path}' kunde inte lösas",
"jsonEmpty": "tomt värde vid json_path",
"dockerNoTags": "inga taggar matchade (docker_hub)",
"dockerMovingOnly": "tag_regex matchade bara rörliga taggar; använd spårning av Docker-avbildens digest",
"dockerHubError": "docker hub-fel {code}",
"dockerHubJson": "ogiltig JSON från docker hub: {detail}",
"dockerHubEmpty": "docker hub returnerade inga taggar",
"payload": "payload måste vara ett JSON-objekt",
"name": "name krävs och måste vara 1–64 tecken av bokstäver/siffror/mellanslag/._+-()/",
"method": "installed_via måste vara en av: {values} eller tom",
"package": "package krävs (bokstäver/siffror/._+@:/ upp till 127 tecken)",
"filePath": "file_path krävs och måste vara en absolut sökväg",
"fileRegex": "file_regex krävs",
"fileRegexInvalid": "file_regex är inte ett giltigt reguljärt uttryck: {detail}",
"binaryPath": "binary_path krävs och måste vara en absolut sökväg",
"pythonPath": "python_path krävs och måste vara en absolut sökväg",
"distribution": "distribution krävs (namn enligt PEP 503)",
"container": "container_name krävs (Dockers namngivningsregler)",
"label": "label krävs (namngivningsregler för OCI-etiketter)",
"dockerBinary": "binary_path krävs (absolut sökväg eller enbart kommando)",
"manualVersion": "installed_version krävs för den manuella metoden",
"versionLength": "installed_version överskrider {max} tecken",
"versionControl": "installed_version innehåller ett otillåtet kontrolltecken",
"installedRegexInvalid": "installed_regex är inte ett giltigt reguljärt uttryck: {detail}",
"upstreamType": "upstream_type måste vara en av: {values}",
"tagRegexInvalid": "tag_regex är inte ett giltigt reguljärt uttryck: {detail}",
"repoRequired": "repo krävs för upstream_type=github",
"repoFormat": "repo måste vara 'owner/name'",
"githubSource": "github_source måste vara en av: {values}",
"upstreamUrlRequired": "upstream_url krävs för upstream_type=http_json",
"upstreamUrl": "upstream_url måste vara en http(s)-URL",
"upstreamUrlLength": "upstream_url överskrider {max} tecken",
"jsonPathRequired": "upstream_json_path krävs för upstream_type=http_json",
"jsonPathLength": "upstream_json_path överskrider {max} tecken",
"jsonPathCharacters": "upstream_json_path använder otillåtna tecken",
"dockerImageRequired": "docker_image krävs för upstream_type=docker_hub",
"dockerImageLength": "docker_image överskrider {max} tecken",
"dockerImageFormat": "docker_image måste följa Docker Hubs namngivning (owner/name eller name)",
"healthPath": "health_path måste vara en giltig URL-sökväg (högst 255 tecken)",
"logoUrl": "logo_url måste vara en http(s)-URL eller den här VM:ens lokalt cachade ikon",
"discoveryId": "ogiltigt discovery_id",
"helperSlug": "helper_slug måste vara en slug med gemener (bokstäver/siffror/._-)",
"updateVia": "update_via accepterar bara 'docker'",
"dockerDetector": "update_via=docker kräver en detektor av typen docker_label eller docker_exec",
"dockerConflict": "update_via=docker kan inte kombineras med {field}",
"updateCommandType": "update_command måste vara en sträng",
"updateCommandLength": "update_command överskrider {max} tecken",
"updateCommandNull": "update_command innehåller en null-byte",
"updateMethod": "update_method måste vara none, helper eller custom",
"updateCommandRequired": "update_command krävs för update_method=custom",
"updateCommandOnlyCustom": "update_command tillåts bara för update_method=custom",
"helperRequired": "en helper_slug som stöds krävs för update_method=helper",
"dockerUpdater": "Docker-delegerade appar uppdateras via sin avbild, inte via en separat appuppdaterare",
"commandRequired": "command_argv krävs (lista som inte är tom)",
"commandList": "command_argv måste vara en lista med strängar som inte är tom",
"commandCount": "command_argv accepterar högst {max} poster",
"commandEntry": "command_argv[{index}] måste vara en sträng som inte är tom",
"commandEntryLength": "command_argv[{index}] överskrider {max} tecken",
"commandEntryControl": "command_argv[{index}] innehåller ett otillåtet kontrolltecken",
"binaryArgsList": "binary_args måste vara en lista med strängar",
"binaryArgsCount": "binary_args accepterar högst {max} poster",
"binaryArgEntry": "binary_args[{index}] måste vara en sträng som inte är tom",
"binaryArgLength": "binary_args[{index}] överskrider {max} tecken",
"binaryArgControl": "binary_args[{index}] innehåller ett otillåtet kontrolltecken",
"portsList": "ports måste vara en lista med {port, description, scheme}",
"portObject": "ports[{index}] måste vara ett objekt",
"portRequired": "ports[{index}].port krävs",
"portInteger": "ports[{index}].port måste vara ett heltal",
"portRange": "ports[{index}].port måste vara 1–65535",
"portDuplicate": "porten {port} förekommer mer än en gång för den här appen",
"portDescription": "ports[{index}].description innehåller ogiltiga tecken",
"portScheme": "ports[{index}].scheme måste vara 'http' eller 'https'",
"portPath": "ports[{index}].web_path måste vara en giltig URL-sökväg (högst 255 tecken)",
"portLogo": "ports[{index}].logo_url måste vara en http(s)-URL (högst 512 tecken)",
"portCategory": "ports[{index}].category innehåller ogiltiga tecken eller är för lång",
"portCustomUrl": "ports[{index}].custom_url måste vara en http(s)-URL (högst 512 tecken)",
"tagRegexLength": "tag_regex överskrider 512 tecken",
"persistPermission": "kunde inte spara sidecar-filen (behörighet?)",
"persist": "kunde inte spara sidecar-filen",
"noApps": "inga appar registrerade för detta vmid",
"appMissing": "app_id '{appId}' hittades inte",
"appNotFound": "appen hittades inte"
}
},
"statusFilter": {
"ariaLabel": "Filtrera virtuella maskiner och containrar",
@@ -2294,6 +2417,42 @@
"updateManyDescription": "Kör om {count} efterinstallationsfunktioner i följd.",
"legacySourceTitle": "Äldre optimering – klicka för att se källan",
"sourceTitle": "Klicka för att se källkoden",
"sourceDialogTitle": "Källkod: {name}",
"sourceLoading": "Läser in källkod…",
"sourceUnavailable": "Källkoden är inte tillgänglig.",
"sourceLoadFailed": "Det gick inte att läsa in källkoden. Försök igen.",
"sourceFunctionMissing": "Funktionen hittades inte i de tillgängliga skripten.",
"sourceUnknownTool": "Okänt verktyg: {tool}",
"sourceNoMapping": "Ingen funktion är kopplad till verktyget {tool}.",
"sourceToolNames": {
"subscription_banner": "Borttagning av prenumerationsbanner",
"time_sync": "Tidssynkronisering",
"apt_languages": "Hoppa över APT-språk",
"journald": "Optimering av journald",
"logrotate": "Optimering av logrotate",
"system_limits": "Höjda systemgränser",
"memory_settings": "Optimering av minnesinställningar",
"zram_swap": "ZRAM-swapprofil",
"kernel_panic": "Konfiguration av kernel panic",
"apt_ipv4": "Tvinga IPv4 för APT",
"kexec": "kexec för snabba omstarter",
"rpc": "Inaktivera RPC / rpcbind",
"motd": "Anpassad MOTD-banner",
"system_utils": "Systemverktyg",
"network_optimization": "Nätverksoptimeringar",
"bashrc_custom": "Anpassning av bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (SSD-skydd)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Korrigeringar för AMD CPU (Ryzen/EPYC)",
"persistent_network": "Beständiga namn på nätverksgränssnitt",
"vfio_iommu": "VFIO/IOMMU-passthrough",
"lvm_repair": "Reparation av LVM PV-huvuden",
"repo_cleanup": "Rensning av arkiv",
"proxmox_repos": "Proxmox APT-arkiv",
"entropy": "Entropigenerering (haveged)"
},
"legacy": "arv",
"updateToolTitle": "Uppdatera {name} till v{version}",
"availableUpdates": "Tillgängliga uppdateringar",
@@ -4820,6 +4979,124 @@
"cancel": "Avbryt",
"save": "Spara bevarandepolicy",
"useDefault": "Använd Proxmox-standard (ingen policy för denna lagring)"
},
"apiErrors": {
"requestFailed": "Förfrågan misslyckades.",
"absolutePath": "sökvägen måste vara absolut",
"pathConfigured": "den här sökvägen är redan konfigurerad",
"pathRequired": "sökväg krävs",
"pathQuery": "frågeparametern path krävs",
"pathNotFound": "sökvägen hittades inte",
"invalidJobId": "ogiltigt jobb-ID",
"jobNotFound": "jobbet hittades inte",
"runnerMissing": "körskriptet är inte installerat",
"noRestore": "inget återställningstillstånd att avfärda",
"invalidFile": "ogiltigt filnamn",
"notFound": "hittades inte",
"logOutside": "loggsökvägen ligger utanför den tillåtna katalogen",
"targetsList": "targets måste vara en lista",
"emptyExpression": "tomt uttryck",
"invalidId": "ogiltigt ID (använd bokstäver, siffror, _ eller -)",
"backend": "backend måste vara pbs, local eller borg",
"remoteBackend": "backend måste vara pbs eller borg",
"remoteBackendQuoted": "backend måste vara 'pbs' eller 'borg'",
"profileMode": "profile_mode måste vara default eller custom",
"borgAttached": "borg kan inte kopplas (PVE vzdump skriver bara till pbs/local-lagringar)",
"borgAttachedLong": "borg är inte en giltig backend för kopplade jobb (PVE vzdump skriver bara till pbs/local-lagringar)",
"parentRequired": "pve_parent_job_id och pve_storage krävs för attached",
"absolutePaths": "paths måste vara en lista med absoluta sökvägar",
"customPath": "en anpassad profil behöver minst en sökväg",
"defaultPaths": "kunde inte lösa standardprofilens sökvägar",
"retentionObject": "retention måste vara ett objekt",
"pbsRepository": "pbs_repository krävs",
"pbsEncryption": "pbs_encrypt_mode måste vara 'none', 'new' eller 'existing'",
"pbsKeyFailed": "det gick inte att skapa PBS-krypteringsnyckelfilen",
"pbsClientMissing": "det gick inte att skapa PBS-krypteringsnyckelfilen (är proxmox-backup-client installerat?)",
"borgRepository": "borg_repo krävs",
"borgEncryption": "borg_encrypt_mode måste vara en av: none, repokey, keyfile, authenticated",
"encryption": "encrypt_mode måste vara en av: none, repokey, keyfile, authenticated",
"borgPassphrase": "borg_passphrase krävs när kryptering är aktiverad (spara den på destinationen eller skicka med en i förfrågan)",
"passphrase": "lösenfras krävs när kryptering är aktiverad",
"password": "lösenord krävs",
"deviceConfirmation": "confirm_device måste exakt matcha device (skydd mot felskrivning)",
"invalidDevice": "ogiltig enhetssökväg",
"absoluteMountpoint": "en absolut monteringspunkt krävs",
"archiveLayout": "arkivets struktur känns inte igen — ingen rootfs/ hittades",
"archiveMissing": "arkivet hittades inte",
"archiveNotFile": "arkivsökvägen är inte en vanlig fil",
"invalidName": "ogiltigt namn",
"invalidStaging": "ogiltig staging_path",
"jobRename": "jobbets namn kan inte ändras (ta bort och skapa på nytt för att byta namn)",
"absoluteKey": "key_path måste vara absolut",
"mode": "mode måste vara \"local\" eller \"ssh\"",
"nameRequired": "namn krävs (bokstäver, siffror, _ eller -)",
"noLog": "ingen logg finns ännu för det här jobbet",
"noPbsConfig": "ingen fil med manuella PBS-konfigurationer",
"outputMissing": "utdatafilen saknas",
"parserJson": "parserns utdata var inte giltig JSON",
"restoreJson": "utdata från run_restore var inte giltig JSON",
"repoRequired": "repo krävs",
"repoSnapshot": "repo_name och snapshot krävs",
"borgRepoSnapshot": "repo_name och snapshot krävs för borg",
"pbsRepoSnapshot": "repo_name och snapshot krävs för pbs",
"repoName": "repo_name krävs",
"restoreMissing": "återställningsverktygen är inte installerade på den här värden",
"serverDatastore": "server och datalager krävs",
"snapshotRequired": "snapshot krävs",
"source": "source måste vara pbs, borg eller local",
"sshPort": "ssh_port måste vara ett heltal mellan 1 och 65535",
"sshRequired": "ssh_user, ssh_host och ssh_remote_path krävs för ssh-läge",
"repositoryPath": "målet saknar arkivsökväg",
"taskExpired": "uppgiften hittades inte eller har gått ut",
"invalidExpression": "ogiltigt uttryck",
"noReport": "ingen rapport skapades",
"parseManifestFailed": "parse_manifest avslutades med en annan kod än noll",
"jobExists": "jobbet finns redan: {value}",
"pathMissing": "sökvägen finns inte på den här värden: {value}",
"deviceMissing": "enheten finns inte: {value}",
"mountpoint": "inte en monteringspunkt: {value}",
"localArchiveMissing": "lokalt arkiv hittades inte: {value}",
"borgDestination": "Borg-destinationen '{value}' hittades inte",
"pbsDestination": "PBS-destinationen '{value}' hittades inte",
"pbsPassword": "inget lösenord tillgängligt för PBS-arkivet '{value}'",
"borgRepoMissing": "Borg-arkivet \"{value}\" är inte konfigurerat",
"pbsRepoMissing": "PBS-arkivet \"{value}\" är inte konfigurerat",
"borgTargetMissing": "Borg-målet hittades inte: {value}",
"pbsTargetMissing": "PBS-arkivet hittades inte: {value}",
"pveJobMissing": "PVE vzdump-jobbet hittades inte: {value}",
"statePersist": "kan inte spara tillståndet: {value}",
"sshDirectory": "kan inte förbereda ssh-katalogen: {value}",
"logRead": "kan inte läsa loggen: {value}",
"envUpdate": "kunde inte uppdatera .env: {value}",
"pbsConfigWrite": "det gick inte att spara PBS-konfigurationen: {value}",
"persist": "det gick inte att spara: {value}",
"archiveRemove": "det gick inte att ta bort arkivet: {value}",
"start": "det gick inte att starta: {value}",
"borgTargetsUpdate": "det gick inte att uppdatera borg-targets.txt: {value}",
"pbsTargetsUpdate": "det gick inte att uppdatera pbs-manual-configs.txt: {value}",
"unitsUpdate": "det gick inte att uppdatera systemd-enheter: {value}",
"borgTargetsWrite": "det gick inte att skriva borg-targets.txt: {value}",
"jobWrite": "det gick inte att skriva jobbfiler: {value}",
"localTargetWrite": "det gick inte att skriva local-target.conf: {value}",
"unitsWrite": "det gick inte att skriva systemd-enheter: {value}",
"format": "formateringen misslyckades: {value}",
"calendar": "ogiltig on_calendar: {value}",
"mount": "monteringen misslyckades: {value}",
"parserInvocation": "anropet av parsern misslyckades: {value}",
"preflightInvocation": "anropet av preflight misslyckades: {value}",
"publicKeyMissing": "den publika nyckeln hittades inte efter generering: {value}",
"retentionInteger": "retention.{value} måste vara ett heltal",
"retentionNegative": "retention.{value} får inte vara negativt",
"keygen": "ssh-keygen misslyckades: {value}",
"systemctl": "systemctl misslyckades: {value}",
"unmount": "umount misslyckades: {value}",
"taskIncomplete": "uppgiften har tillståndet \"{value}\", inte slutförd",
"unknownKind": "okänd typ: {value}",
"unknownMode": "okänt läge \"{value}\"",
"defaultRemove": "{value} är den inbyggda standarden och kan inte tas bort",
"defaultAdd": "{value} är den inbyggda standarden; den behöver inte läggas till",
"keyImport": "Ingen PBS-nyckelfil är installerad i {value}. Importera nyckelfilen först.",
"storageMismatch": "lagringen för PVE-jobbet stämmer inte (jobbet använder {value}, payload anger {other})"
}
},
"healthStatus": {
@@ -5320,6 +5597,7 @@
"websocketError": "WebSocket-fel uppstod",
"connectionClosed": "Anslutning stängd",
"completedSuccessfully": "Skriptet har slutförts",
"cancelled": "Åtgärden avbröts.",
"completedWithError": "Skriptet avslutat med felkoden {code}",
"yes": "Ja",
"yourInput": "Din inmatning:",
@@ -7311,7 +7589,7 @@
"signInWith": "Logga in med",
"methodPassword": "Användare och lösenord",
"methodToken": "API-token",
"username": "Användare (användare@realm)",
"username": "Användare",
"password": "Lösenord",
"passwordHelp": "Lösenordet används en gång för att skapa en skrivskyddad API-token (rollen Audit) för den här Monitorn. Bara den token sparas, aldrig lösenordet. Aktivera först autentiseringen i Monitor.",
"cancel": "Avbryt",
@@ -7324,10 +7602,10 @@
"editConnection": "Anslutning att redigera",
"add": "Lägg till en ny anslutning",
"name": "Namn",
"url": "HTTPS-URL (ta med :8007 för PBS ordinarie port)",
"token_id": "API-token-ID (user@realm!token)",
"token_secret": "API-tokenhemlighet (lämna tomt för att behålla den sparade hemligheten)",
"fingerprint": "Certifikatets SHA256-fingeravtryck (valfritt)",
"url": "HTTPS-adress",
"token_id": "API-token-ID",
"token_secret": "API-tokenhemlighet",
"fingerprint": "SHA256-fingeravtryck (valfritt)",
"tlsHelp": "Lämna fingeravtrycket tomt för vanlig kontroll av certifikatutfärdare och värdnamn. För ett självsignerat certifikat kopierar du dess SHA256-fingeravtryck från PBS-konsolen eller en annan betrodd kanal. Fingeravtrycket kontrolleras innan inloggningsuppgifter skickas.",
"saved": "PBS-anslutningen har verifierats och sparats.",
"saveFailed": "Det gick inte att spara PBS-anslutningen.",
@@ -7369,7 +7647,85 @@
"estimatedFull": "Beräknad full",
"estimatedFullUnknown": "Inte tillräckligt med data ännu",
"duration": "Varaktighet",
"noJobs": "Inga synkroniserings-, verifierings- eller beskärningsscheman är konfigurerade."
"noJobs": "Inga synkroniserings-, verifierings- eller beskärningsscheman är konfigurerade.",
"errors": {
"configRead": "Det gick inte att läsa de sparade PBS-anslutningarna. Kontrollera konfigurationsfilen och behörigheterna.",
"invalidConfig": "Inställningarna för PBS-anslutningen är inte giltiga. Kontrollera formuläret och försök igen.",
"invalidField": "Ett anslutningsfält är ogiltigt eller för långt.",
"invalidUrl": "Använd en HTTPS-adress till PBS utan inbäddade inloggningsuppgifter, frågeparametrar eller fragment.",
"nameRequired": "Ange ett servernamn på högst 128 tecken.",
"tokenIdFormat": "Ange ID för PBS API-token i formatet user@realm!token.",
"tokenSecret": "Ange en giltig API-tokenhemlighet utan mellanslag eller kontrolltecken.",
"invalidTokenId": "API-token-ID får bara innehålla ASCII-tecken.",
"invalidFingerprint": "Ange certifikatets SHA256-fingeravtryck: 64 hexadecimala tecken, eventuellt avgränsade med kolon.",
"fingerprintMismatch": "PBS-certifikatet matchar inte fingeravtrycket. Inga inloggningsuppgifter skickades.",
"tokenRejected": "PBS avvisade API-token eller dess behörigheter. Kontrollera ACL:erna för både användaren och token.",
"invalidResponse": "PBS returnerade ett oväntat svar. Omdirigeringar följs inte.",
"invalidJson": "PBS returnerade ett ogiltigt JSON-dokument.",
"connectFailed": "Det gick inte att ansluta till PBS via verifierad HTTPS. Kontrollera adressen, certifikatet och tjänsten.",
"invalidId": "ID:t för PBS-anslutningen är inte giltigt. Läs in anslutningslistan igen.",
"invalidDatastores": "PBS returnerade en ogiltig lista över datalager.",
"connectionLimit": "Högst {count} PBS-anslutningar kan konfigureras.",
"username": "Ange PBS-användaren i formatet user@realm, till exempel root@pam.",
"password": "Ange PBS-användarens lösenord.",
"loginRejected": "PBS avvisade användarnamnet eller lösenordet.",
"invalidLogin": "PBS returnerade ett ogiltigt inloggningssvar.",
"tokenCreateDenied": "Den här PBS-användaren kan inte skapa API-tokens.",
"invalidToken": "PBS returnerade en ogiltig API-token.",
"auditRoleDenied": "Den här PBS-användaren kan inte tilldela rollen Audit. Logga in som root@pam eller en annan administratör.",
"notFound": "PBS-anslutningen hittades inte. Läs in anslutningslistan igen.",
"readFailed": "Det gick inte att läsa PBS-data. Kontrollera tjänsten och behörigheterna.",
"invalidList": "PBS returnerade en ogiltig datalista.",
"localUnavailable": "PBS är inte tillgänglig på den här värden.",
"authRequired": "Aktivera lokal autentisering i Monitor innan du hanterar fjärranslutningar till PBS.",
"datastoreOffline": "Datalagret är offline, i underhållsläge eller så är dess underliggande enhet inte monterad.",
"datastoreUnavailable": "Datalagrets filsystem är inte tillgängligt."
},
"taskTypes": {
"backup": "Säkerhetskopiering",
"restore": "Återställning",
"verify": "Verifiering",
"verifyGroup": "Verifiering av säkerhetskopiegrupp",
"verifySnapshot": "Verifiering av ögonblicksbild",
"prune": "Gallring",
"garbageCollection": "Skräpinsamling",
"sync": "Synkronisering",
"updateCheck": "Sök efter uppdateringar",
"terminal": "Terminal",
"createDatastore": "Skapa datalager",
"other": "Annan uppgift ({type})"
},
"states": {
"ok": "OK",
"unknown": "Okänd",
"warnings": "Varningar: {count}",
"warning": "Varning",
"error": "Fel"
},
"serviceStates": {
"active": "Aktiv",
"inactive": "Inaktiv",
"failed": "Misslyckad",
"activating": "Startar",
"deactivating": "Stoppar",
"unknown": "Okänd"
},
"durationUnits": {
"seconds": "{count} s",
"minutes": "{count} min",
"hours": "{count} h",
"days": "{count} d"
},
"urlHelp": "Ta med :8007 för PBS standardport, till exempel https://pbs.example:8007.",
"usernameHelp": "Använd user@realm, till exempel root@pam.",
"tokenIdHelp": "Använd user@realm!token, till exempel monitor@pbs!proxmenux.",
"tokenSecretHelp": "Ange hemligheten för den här API-token.",
"tokenSecretKeepHelp": "Lämna tomt för att behålla den sparade tokenhemligheten.",
"removeFailed": "Det gick inte att ta bort PBS-anslutningen.",
"savedRefreshFailed": "PBS-anslutningen sparades, men anslutningslistan kunde inte uppdateras. Läs in sidan igen innan du gör en annan ändring.",
"removedRefreshFailed": "PBS-anslutningen togs bort, men anslutningslistan kunde inte uppdateras. Läs in sidan igen.",
"technicalDetails": "Tekniska detaljer",
"datastoreType": "Datalager"
},
"monitorLinks": {
"groups": {
@@ -7408,6 +7764,17 @@
"addSelected": "Lägg till valda",
"pbsWebInterface": "PBS webbgränssnitt",
"changeAddress": "Ändra adress",
"cancel": "Avbryt"
"cancel": "Avbryt",
"errors": {
"addressRequired": "Ange adressen till Monitor.",
"invalidAddress": "Adressen måste börja med http:// eller https:// och ange en värd.",
"addressCredentials": "Adressen får inte innehålla användare eller lösenord.",
"invalidPort": "Adressens port är inte giltig.",
"invalidRequest": "Förfrågan är inte giltig. Läs in sidan igen och försök igen.",
"limit": "Högst {count} noder kan läggas till.",
"duplicate": "Den här adressen finns redan i listan.",
"notFound": "Noden hittades inte. Läs in listan igen och försök igen.",
"detectedRemove": "Endast noder som lagts till manuellt kan tas bort. Identifierade noder kan döljas."
}
}
}
+13 -6
View File
@@ -7,6 +7,13 @@ from jwt_middleware import require_auth, require_admin_scope
monitor_links_bp = Blueprint("monitor_links", __name__)
def link_error(exc):
payload = {"error": str(exc), "code": exc.code}
if exc.params:
payload["params"] = exc.params
return jsonify(payload), 400
@monitor_links_bp.after_request
def no_cache(response):
response.headers["Cache-Control"] = "no-store"
@@ -27,9 +34,9 @@ def add_link():
monitor_links.refresh()
return jsonify(success=True, key=key)
except monitor_links.LinkError as exc:
return jsonify(error=str(exc)), 400
return link_error(exc)
except OSError:
return jsonify(error="Could not save the node list."), 500
return jsonify(error="Could not save the node list.", code="monitor_links_save_failed"), 500
@monitor_links_bp.route("/api/monitor-links/<path:key>", methods=["PATCH"])
@@ -40,9 +47,9 @@ def update_link(key):
monitor_links.refresh()
return jsonify(success=True)
except monitor_links.LinkError as exc:
return jsonify(error=str(exc)), 400
return link_error(exc)
except OSError:
return jsonify(error="Could not save the node list."), 500
return jsonify(error="Could not save the node list.", code="monitor_links_save_failed"), 500
@monitor_links_bp.route("/api/monitor-links/<path:key>", methods=["DELETE"])
@@ -53,6 +60,6 @@ def delete_link(key):
monitor_links.refresh()
return jsonify(success=True)
except monitor_links.LinkError as exc:
return jsonify(error=str(exc)), 400
return link_error(exc)
except OSError:
return jsonify(error="Could not save the node list."), 500
return jsonify(error="Could not save the node list.", code="monitor_links_save_failed"), 500
+15 -11
View File
@@ -8,6 +8,10 @@ from jwt_middleware import require_auth, require_admin_scope
pbs_bp = Blueprint("pbs", __name__)
def pbs_error(exc, status):
return jsonify(error=str(exc), **exc.problem()), status
@pbs_bp.after_request
def no_cache(response):
response.headers["Cache-Control"] = "no-store"
@@ -27,7 +31,7 @@ def servers():
servers = pbs.public_servers()
return jsonify(servers=servers, local_available=pbs.local_available(), detected=_detected(servers))
except pbs.PBSError as exc:
return jsonify(error=str(exc)), 500
return pbs_error(exc, 500)
def _detected(servers):
@@ -57,42 +61,42 @@ def _protected_configuration():
@require_admin_scope
def save_server():
if not _protected_configuration():
return jsonify(error="Enable local authentication before saving remote PBS credentials."), 409
return jsonify(error="Enable local authentication before saving remote PBS credentials.", code="pbs_auth_required"), 409
try:
identifier = pbs.save_server(request.get_json(silent=True))
return jsonify(success=True, id=identifier, servers=pbs.public_servers())
except pbs.PBSError as exc:
return jsonify(error=str(exc)), 400
return pbs_error(exc, 400)
except OSError:
return jsonify(error="Could not save PBS connection."), 500
return jsonify(error="Could not save PBS connection.", code="pbs_save_failed"), 500
@pbs_bp.route("/api/pbs/servers/login", methods=["POST"])
@require_admin_scope
def connect_with_password():
if not _protected_configuration():
return jsonify(error="Enable local authentication before saving remote PBS credentials."), 409
return jsonify(error="Enable local authentication before saving remote PBS credentials.", code="pbs_auth_required"), 409
try:
identifier = pbs.connect_with_password(request.get_json(silent=True))
return jsonify(success=True, id=identifier, servers=pbs.public_servers())
except pbs.PBSError as exc:
return jsonify(error=str(exc)), 400
return pbs_error(exc, 400)
except OSError:
return jsonify(error="Could not save PBS connection."), 500
return jsonify(error="Could not save PBS connection.", code="pbs_save_failed"), 500
@pbs_bp.route("/api/pbs/servers/<identifier>", methods=["DELETE"])
@require_admin_scope
def delete_server(identifier):
if not _protected_configuration():
return jsonify(error="Enable local authentication before changing PBS connections."), 409
return jsonify(error="Enable local authentication before changing PBS connections.", code="pbs_auth_required"), 409
try:
pbs.delete_server(identifier)
return jsonify(success=True)
except pbs.PBSError as exc:
return jsonify(error=str(exc)), 400
return pbs_error(exc, 400)
except OSError:
return jsonify(error="Could not remove PBS connection."), 500
return jsonify(error="Could not remove PBS connection.", code="pbs_remove_failed"), 500
@pbs_bp.route("/api/pbs/status")
@@ -101,4 +105,4 @@ def status():
try:
return jsonify(pbs.get_status(request.args.get("server", "local")))
except pbs.PBSError as exc:
return jsonify(error=str(exc)), 502
return pbs_error(exc, 502)
+15 -12
View File
@@ -39,7 +39,10 @@ _refresh_lock = threading.Lock()
class LinkError(ValueError):
pass
def __init__(self, message, *, code="monitor_links_save_failed", params=None):
super().__init__(message)
self.code = code
self.params = params
def _load():
@@ -63,19 +66,19 @@ def _save(data):
def normalize_url(value):
"""Return the origin of an http(s) address, or raise LinkError."""
if not isinstance(value, str) or not value.strip():
raise LinkError("Enter the address of the Monitor.")
raise LinkError("Enter the address of the Monitor.", code="monitor_link_address_required")
text = value.strip()
if "://" not in text:
text = "https://" + text
parsed = urllib.parse.urlsplit(text)
if parsed.scheme not in ("http", "https") or not parsed.hostname:
raise LinkError("The address must start with http:// or https:// and name a host.")
raise LinkError("The address must start with http:// or https:// and name a host.", code="monitor_link_invalid_address")
if parsed.username or parsed.password:
raise LinkError("The address must not contain a user or password.")
raise LinkError("The address must not contain a user or password.", code="monitor_link_address_credentials")
try:
port = parsed.port
except ValueError as exc:
raise LinkError("The port of the address is not valid.") from exc
raise LinkError("The port of the address is not valid.", code="monitor_link_invalid_port") from exc
host = parsed.hostname
if ":" in host:
host = f"[{host}]"
@@ -301,16 +304,16 @@ def start_prewarmer():
def add_link(payload):
if not isinstance(payload, dict):
raise LinkError("Invalid request.")
raise LinkError("Invalid request.", code="monitor_link_invalid_request")
name = str(payload.get("name") or "").strip()[:64]
url = normalize_url(payload.get("url"))
product = payload.get("product") if payload.get("product") in PRODUCTS else "pve"
with _lock:
data = _load()
if len(data["manual"]) >= MAX_MANUAL:
raise LinkError(f"At most {MAX_MANUAL} nodes can be added.")
raise LinkError(f"At most {MAX_MANUAL} nodes can be added.", code="monitor_link_limit", params={"count": MAX_MANUAL})
if any(row.get("url") == url for row in data["manual"]):
raise LinkError("This address is already in the list.")
raise LinkError("This address is already in the list.", code="monitor_link_duplicate")
identifier = secrets.token_hex(6)
data["manual"].append({"id": identifier, "name": name or urllib.parse.urlsplit(url).hostname,
"url": url, "product": product})
@@ -320,13 +323,13 @@ def add_link(payload):
def update_link(key, payload):
if not isinstance(payload, dict):
raise LinkError("Invalid request.")
raise LinkError("Invalid request.", code="monitor_link_invalid_request")
with _lock:
data = _load()
if key.startswith("manual:"):
row = next((r for r in data["manual"] if f"manual:{r.get('id')}" == key), None)
if row is None:
raise LinkError("Node not found.")
raise LinkError("Node not found.", code="monitor_link_not_found")
if "name" in payload:
row["name"] = str(payload["name"] or "").strip()[:64] or row["name"]
if payload.get("url"):
@@ -345,7 +348,7 @@ def update_link(key, payload):
if not override:
data["overrides"].pop(key, None)
else:
raise LinkError("Node not found.")
raise LinkError("Node not found.", code="monitor_link_not_found")
_save(data)
@@ -354,6 +357,6 @@ def delete_link(key):
data = _load()
remaining = [r for r in data["manual"] if f"manual:{r.get('id')}" != key]
if len(remaining) == len(data["manual"]):
raise LinkError("Only nodes added by hand can be removed; detected ones can be hidden.")
raise LinkError("Only nodes added by hand can be removed; detected ones can be hidden.", code="monitor_link_detected_remove")
data["manual"] = remaining
_save(data)
+44 -34
View File
@@ -39,7 +39,13 @@ MAX_RESPONSE = 4 * 1024 * 1024
class PBSError(ValueError):
pass
def __init__(self, message, *, code="pbs_unknown", params=None):
super().__init__(message)
self.code = code
self.params = params
def problem(self):
return {"code": self.code, **({"params": self.params} if self.params else {})}
def get_platform():
@@ -87,7 +93,7 @@ def _load_servers():
except FileNotFoundError:
return []
except (OSError, ValueError) as exc:
raise PBSError("Could not read the PBS server configuration.") from exc
raise PBSError("Could not read the PBS server configuration.", code="pbs_config_read") from exc
def _save_servers(servers):
@@ -113,11 +119,11 @@ def public_servers():
def _validate_server(data, previous=None, fields=("name", "url", "token_id", "token_secret", "fingerprint")):
if not isinstance(data, dict):
raise PBSError("Invalid PBS server configuration.")
raise PBSError("Invalid PBS server configuration.", code="pbs_invalid_config")
previous = previous or {}
row = {field: data.get(field, previous.get(field, "")) for field in fields}
if any(not isinstance(value, str) or len(value) > 4096 for value in row.values()):
raise PBSError("Invalid PBS connection field.")
raise PBSError("Invalid PBS connection field.", code="pbs_invalid_field")
row["name"] = row["name"].strip()
row["url"] = row["url"].strip().rstrip("/")
try:
@@ -127,26 +133,26 @@ def _validate_server(data, previous=None, fields=("name", "url", "token_id", "to
or parts.query or parts.fragment):
raise ValueError()
except ValueError as exc:
raise PBSError("Use an HTTPS PBS URL without embedded credentials, query or fragment.") from exc
raise PBSError("Use an HTTPS PBS URL without embedded credentials, query or fragment.", code="pbs_invalid_url") from exc
if not row["name"] or len(row["name"]) > 128:
raise PBSError("A server name of at most 128 characters is required.")
raise PBSError("A server name of at most 128 characters is required.", code="pbs_name_required")
if "token_id" not in row:
return _validated_fingerprint(row)
row["token_id"] = row["token_id"].strip()
if not re.fullmatch(r"[^\s:!]+@[^\s:!]+![^\s:!]+", row["token_id"]):
raise PBSError("Enter a PBS API token ID in user@realm!token format.")
raise PBSError("Enter a PBS API token ID in user@realm!token format.", code="pbs_token_id_format")
if (not row["token_secret"] or not row["token_secret"].isascii()
or any(char.isspace() or ord(char) < 32 for char in row["token_secret"])):
raise PBSError("A valid API token secret is required.")
raise PBSError("A valid API token secret is required.", code="pbs_token_secret")
if not row["token_id"].isascii():
raise PBSError("Invalid API token ID.")
raise PBSError("Invalid API token ID.", code="pbs_invalid_token_id")
return _validated_fingerprint(row)
def _validated_fingerprint(row):
fingerprint = row["fingerprint"].strip().lower().replace(":", "")
if fingerprint and not re.fullmatch("[a-f0-9]{64}", fingerprint):
raise PBSError("The certificate fingerprint must be SHA256 (64 hex characters).")
raise PBSError("The certificate fingerprint must be SHA256 (64 hex characters).", code="pbs_invalid_fingerprint")
row["fingerprint"] = fingerprint
return row
@@ -158,7 +164,8 @@ def _remote_get(server, endpoint):
def _remote_request(server, method, endpoint, headers, form=None,
rejected="PBS rejected the API token or its permissions. Check both the user and token ACLs."):
rejected="PBS rejected the API token or its permissions. Check both the user and token ACLs.",
*, rejected_code="pbs_token_rejected"):
"""One request to PBS; credentials leave only after the certificate is checked."""
parts = urlsplit(server["url"])
fingerprint = server.get("fingerprint")
@@ -175,7 +182,7 @@ def _remote_request(server, method, endpoint, headers, form=None,
if fingerprint:
actual = hashlib.sha256(connection.sock.getpeercert(binary_form=True)).hexdigest()
if not hmac.compare_digest(actual, fingerprint):
raise PBSError("PBS certificate fingerprint does not match. No credentials were sent.")
raise PBSError("PBS certificate fingerprint does not match. No credentials were sent.", code="pbs_fingerprint_mismatch")
path = parts.path.rstrip("/") + "/api2/json" + endpoint
body = urlencode(form) if form is not None else None
request_headers = {"Accept": "application/json", **headers}
@@ -185,17 +192,17 @@ def _remote_request(server, method, endpoint, headers, form=None,
response = connection.getresponse()
content = response.read(MAX_RESPONSE + 1)
if response.status in (401, 403):
raise PBSError(rejected)
raise PBSError(rejected, code=rejected_code)
if response.status != 200 or len(content) > MAX_RESPONSE:
raise PBSError("PBS returned an invalid response. Redirects are not followed.")
raise PBSError("PBS returned an invalid response. Redirects are not followed.", code="pbs_invalid_response")
document = json.loads(content)
if not isinstance(document, dict) or "data" not in document:
raise PBSError("PBS returned an invalid JSON document.")
raise PBSError("PBS returned an invalid JSON document.", code="pbs_invalid_json")
return document["data"]
except PBSError:
raise
except (OSError, http.client.HTTPException, ValueError) as exc:
raise PBSError("Could not connect to PBS over verified HTTPS. Check the address, certificate and service.") from exc
raise PBSError("Could not connect to PBS over verified HTTPS. Check the address, certificate and service.", code="pbs_connect_failed") from exc
finally:
connection.close()
@@ -203,19 +210,19 @@ def _remote_request(server, method, endpoint, headers, form=None,
def save_server(data):
identifier = data.get("id") if isinstance(data, dict) else None
if identifier is not None and not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", str(identifier)):
raise PBSError("Invalid PBS server ID.")
raise PBSError("Invalid PBS server ID.", code="pbs_invalid_id")
with _lock:
previous = next((row for row in _load_servers() if row["id"] == identifier), None)
row = _validate_server(data, previous)
# /version may be public; validate credentials on the protected status API.
usage = _remote_get(row, "/status/datastore-usage")
if not isinstance(usage, list):
raise PBSError("PBS returned an invalid datastore list.")
raise PBSError("PBS returned an invalid datastore list.", code="pbs_invalid_datastores")
row.update(id=identifier or "pbs-" + secrets.token_hex(6), revision=secrets.token_hex(16))
with _lock:
servers = _load_servers()
if previous is None and len(servers) >= 32:
raise PBSError("At most 32 PBS connections may be configured.")
raise PBSError("At most 32 PBS connections may be configured.", code="pbs_connection_limit", params={"count": 32})
servers = [existing for existing in servers if existing["id"] != row["id"]]
_save_servers([*servers, row])
return row["id"]
@@ -228,27 +235,27 @@ def connect_with_password(data):
username = data.get("username") if isinstance(data, dict) else None
password = data.get("password") if isinstance(data, dict) else None
if not isinstance(username, str) or not re.fullmatch(r"[^\s:!@]+@[^\s:!@]+", username.strip()):
raise PBSError("Enter the PBS user as user@realm, for example root@pam.")
raise PBSError("Enter the PBS user as user@realm, for example root@pam.", code="pbs_username")
if not isinstance(password, str) or not password or len(password) > 4096:
raise PBSError("Enter the password of the PBS user.")
raise PBSError("Enter the password of the PBS user.", code="pbs_password")
ticket = _remote_request(endpoint, "POST", "/access/ticket", {},
{"username": username.strip(), "password": password},
rejected="PBS rejected the user name or password.")
rejected="PBS rejected the user name or password.", rejected_code="pbs_login_rejected")
if not isinstance(ticket, dict) or not ticket.get("ticket") or not ticket.get("CSRFPreventionToken"):
raise PBSError("PBS returned an invalid sign-in response.")
raise PBSError("PBS returned an invalid sign-in response.", code="pbs_invalid_login")
session = {"Cookie": "PBSAuthCookie=" + quote(ticket["ticket"], safe=""),
"CSRFPreventionToken": ticket["CSRFPreventionToken"]}
user = str(ticket.get("username") or username.strip())
name = "proxmenux-" + secrets.token_hex(4)
token_path = f"/access/users/{quote(user, safe='')}/token/{name}"
created = _remote_request(endpoint, "POST", token_path, session, {"comment": "ProxMenux Monitor, read-only"},
rejected="This PBS user cannot create API tokens.")
rejected="This PBS user cannot create API tokens.", rejected_code="pbs_token_create_denied")
if not isinstance(created, dict) or not created.get("tokenid") or not created.get("value"):
raise PBSError("PBS returned an invalid API token.")
raise PBSError("PBS returned an invalid API token.", code="pbs_invalid_token")
try:
_remote_request(endpoint, "PUT", "/access/acl", session,
{"path": "/", "role": "Audit", "auth-id": created["tokenid"], "propagate": "1"},
rejected="This PBS user cannot grant the Audit role. Sign in with root@pam or an administrator.")
rejected="This PBS user cannot grant the Audit role. Sign in with root@pam or an administrator.", rejected_code="pbs_audit_role_denied")
return save_server({**endpoint, "token_id": created["tokenid"], "token_secret": created["value"]})
except PBSError:
try:
@@ -263,7 +270,7 @@ def delete_server(identifier):
servers = _load_servers()
remaining = [row for row in servers if row["id"] != identifier]
if len(remaining) == len(servers):
raise PBSError("PBS server not found.")
raise PBSError("PBS server not found.", code="pbs_not_found")
_save_servers(remaining)
@@ -276,7 +283,7 @@ def _cli(*arguments):
raise ValueError()
return data
except (OSError, subprocess.SubprocessError, ValueError) as exc:
raise PBSError("Could not read PBS " + " ".join(arguments[:2]) + ". Check its service and permissions.") from exc
raise PBSError("Could not read PBS " + " ".join(arguments[:2]) + ". Check its service and permissions.", code="pbs_read_failed") from exc
def _local_api(path):
@@ -290,7 +297,7 @@ def _local_api(path):
raise ValueError()
return data
except (OSError, subprocess.SubprocessError, ValueError) as exc:
raise PBSError("Could not read PBS " + path + ". Check its service and permissions.") from exc
raise PBSError("Could not read PBS " + path + ". Check its service and permissions.", code="pbs_read_failed") from exc
def _local_datastores():
@@ -305,12 +312,14 @@ def _local_datastores():
inactive = store.get("backing-device") and not (path and os.path.ismount(path))
if not path or inactive or store.get("maintenance-mode"):
row["error"] = "Datastore is offline, in maintenance or has no mounted backing device."
row["error_code"] = "pbs_datastore_offline"
else:
try:
capacity = shutil.disk_usage(path)
row.update(total=capacity.total, used=capacity.used, avail=capacity.free)
except OSError:
row["error"] = "Datastore filesystem is not available."
row["error_code"] = "pbs_datastore_unavailable"
result.append(row)
return result
@@ -331,10 +340,10 @@ def get_status(identifier):
cached = _status_cache.get(identifier)
if cached and cached[0] > time.monotonic():
return cached[1]
errors = {}
errors, error_details = {}, {}
if identifier == "local":
if not local_available():
raise PBSError("The local host is not a PBS server.")
raise PBSError("The local host is not a PBS server.", code="pbs_local_unavailable")
name, version = get_platform().get("hostname") or socket.gethostname(), _local_version()
services = _local_services()
operations = {"datastores": _local_datastores,
@@ -346,7 +355,7 @@ def get_status(identifier):
with _lock:
server = next((row for row in _load_servers() if row["id"] == identifier), None)
if not server:
raise PBSError("PBS server not found.")
raise PBSError("PBS server not found.", code="pbs_not_found")
metadata = _remote_get(server, "/version")
name, version = server["name"], metadata.get("version") if isinstance(metadata, dict) else None
services = {}
@@ -354,18 +363,19 @@ def get_status(identifier):
"sync": "/admin/sync", "verify": "/admin/verify", "prune": "/admin/prune"}
operations = {key: (lambda endpoint=path: _remote_get(server, endpoint)) for key, path in endpoints.items()}
result = {"server": identifier, "name": name, "version": version, "services": services,
"fetched_at": int(time.time()), "errors": errors}
"fetched_at": int(time.time()), "errors": errors, "error_details": error_details}
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {key: executor.submit(operation) for key, operation in operations.items()}
for key, future in futures.items():
try:
data = future.result()
if not isinstance(data, list) or not all(isinstance(row, dict) for row in data):
raise PBSError("PBS returned an invalid list.")
raise PBSError("PBS returned an invalid list.", code="pbs_invalid_list")
result[key] = [{**row, "name": row.get("store", row.get("name"))} for row in data] if key == "datastores" else data
except PBSError as exc:
result[key] = None # Unknown/forbidden is never presented as zero.
errors[key] = str(exc)
error_details[key] = exc.problem()
with _lock:
if len(_status_cache) >= 33:
_status_cache.clear()
@@ -0,0 +1,138 @@
"""Exercise the shipped routes and persistence; localization is additive."""
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import types
import unittest
from unittest import mock
from flask import Flask
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT / "AppImage/scripts"))
import monitor_links as links
class MonitorLinksI18nTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.path = Path(temporary.name) / "links.json"
self.previous = {"manual": [{"id": "saved", "name": "Saved", "url": "https://saved.test", "product": "pve"}], "overrides": {}}
self.path.write_text(json.dumps(self.previous))
for patch in [mock.patch.object(links, "LINKS_FILE", self.path),
mock.patch.object(links, "refresh"), mock.patch.object(links, "_probe")]:
patch.start()
self.addCleanup(patch.stop)
auth_provider = types.SimpleNamespace(
load_auth_config=lambda: {"enabled": True},
verify_token=lambda token: "fixture" if token in ("admin", "reader") else None,
verify_token_full=lambda token: ("fixture", "full_admin" if token == "admin" else "read_only")
if token in ("admin", "reader") else (None, None))
middleware_spec = importlib.util.spec_from_file_location("fixture_jwt_middleware", ROOT / "AppImage/scripts/jwt_middleware.py")
middleware = importlib.util.module_from_spec(middleware_spec)
# Execute the shipped decorators; only credential verification is faked.
with mock.patch.dict(sys.modules, {"auth_manager": auth_provider}):
middleware_spec.loader.exec_module(middleware)
spec = importlib.util.spec_from_file_location("fixture_monitor_links_routes", ROOT / "AppImage/scripts/flask_monitor_links_routes.py")
routes = importlib.util.module_from_spec(spec)
# Import isolation keeps the real JWT module and other suites untouched.
with mock.patch.dict(sys.modules, {"jwt_middleware": middleware}):
spec.loader.exec_module(routes)
app = Flask(__name__)
app.register_blueprint(routes.monitor_links_bp)
self.client = app.test_client()
def send(self, method, path="", payload=None, role="admin"):
return self.client.open("/api/monitor-links" + path, method=method,
json=payload, headers={"Authorization": "Bearer " + role} if role else {})
def rejected(self, method, path, payload, message, code, params=None):
before = self.path.read_bytes()
with mock.patch.object(links, "_save") as save:
response = self.send(method, path, payload)
self.assertEqual(response.status_code, 400)
expected = {"error": message, "code": code}
if params:
expected["params"] = params
self.assertEqual(response.json, expected)
self.assertEqual(response.headers["Cache-Control"], "no-store")
save.assert_not_called()
links.refresh.assert_not_called()
links._probe.assert_not_called()
self.assertEqual(self.path.read_bytes(), before)
def test_validation_errors_preserve_english_status_and_have_stable_codes_without_effects(self):
cases = [
("POST", "", None, "Invalid request.", "monitor_link_invalid_request"),
("PATCH", "/manual:saved", [], "Invalid request.", "monitor_link_invalid_request"),
("POST", "", {"url": " "}, "Enter the address of the Monitor.", "monitor_link_address_required"),
("POST", "", {"url": "ftp://example.test"}, "The address must start with http:// or https:// and name a host.", "monitor_link_invalid_address"),
("POST", "", {"url": "https://user:pass@example.test"}, "The address must not contain a user or password.", "monitor_link_address_credentials"),
("PATCH", "/manual:saved", {"name": "Unsaved", "url": "https://example.test:99999"}, "The port of the address is not valid.", "monitor_link_invalid_port"),
("PATCH", "/cluster:node", {"url": "ftp://example.test"}, "The address must start with http:// or https:// and name a host.", "monitor_link_invalid_address"),
("POST", "", {"url": "https://saved.test/path"}, "This address is already in the list.", "monitor_link_duplicate"),
("PATCH", "/manual:absent", {"url": "https://new.test"}, "Node not found.", "monitor_link_not_found"),
("PATCH", "/other:node", {}, "Node not found.", "monitor_link_not_found"),
("DELETE", "/cluster:node", None, "Only nodes added by hand can be removed; detected ones can be hidden.", "monitor_link_detected_remove"),
]
for case in cases:
with self.subTest(case=case):
self.rejected(*case)
def test_limit_uses_actual_count_and_preserves_previous_file(self):
with mock.patch.object(links, "MAX_MANUAL", 1):
self.rejected("POST", "", {"url": "https://new.test"}, "At most 1 nodes can be added.",
"monitor_link_limit", {"count": 1})
def test_write_failure_is_localizable_without_leaking_filesystem_details(self):
for method, path, payload in [("POST", "", {"url": "https://new.test"}),
("PATCH", "/manual:saved", {"name": "Unsaved"}),
("DELETE", "/manual:saved", None)]:
with self.subTest(method=method), mock.patch.object(links.os, "replace", side_effect=OSError("private path details")):
before = self.path.read_bytes()
response = self.send(method, path, payload)
self.assertEqual(response.status_code, 500)
self.assertEqual(response.json, {"error": "Could not save the node list.", "code": "monitor_links_save_failed"})
self.assertEqual(self.path.read_bytes(), before)
links.refresh.assert_not_called()
links._probe.assert_not_called()
def test_successful_crud_and_detected_hide_reset_keep_existing_contract(self):
response = self.send("POST", payload={"name": "New", "url": "new.test:8008/path", "product": "pbs"})
self.assertEqual(response.status_code, 200)
self.assertEqual(set(response.json), {"success", "key"})
key = response.json["key"]
self.assertTrue(key.startswith("manual:"))
row = links._load()["manual"][-1]
self.assertEqual((row["url"], row["product"]), ("https://new.test:8008", "pbs"))
self.assertEqual(self.send("PATCH", "/" + key, {"url": "http://changed.test"}).json, {"success": True})
self.assertEqual(links._load()["manual"][-1]["url"], "http://changed.test")
self.assertEqual(self.send("PATCH", "/cluster:node", {"url": "https://override.test", "hidden": True}).json, {"success": True})
self.assertEqual(self.send("PATCH", "/cluster:node", {"url": "", "hidden": False}).json, {"success": True})
self.assertEqual(links._load()["overrides"], {"cluster:node": {"hidden": False}})
self.assertEqual(self.send("DELETE", "/" + key).json, {"success": True})
self.assertEqual(links._load()["manual"], self.previous["manual"])
self.assertEqual(self.path.stat().st_mode & 0o777, 0o600)
self.assertEqual(links.refresh.call_count, 5)
links._probe.assert_not_called()
def test_existing_read_and_admin_decorators_still_guard_routes_before_effects(self):
with mock.patch.object(links, "cached_links", return_value={"self": {}, "links": []}) as cached, \
mock.patch.object(links, "_save") as save:
for method, path in [("POST", ""), ("PATCH", "/manual:saved"), ("DELETE", "/manual:saved")]:
for role, status in [(None, 401), ("reader", 403)]:
self.assertEqual(self.send(method, path, {"url": "https://new.test"}, role).status_code, status)
self.assertEqual(self.send("GET", role=None).status_code, 401)
self.assertEqual(self.send("GET", "?all=1", role="reader").status_code, 200)
cached.assert_called_once_with(include_hidden=True)
save.assert_not_called()
links.refresh.assert_not_called()
links._probe.assert_not_called()
self.assertEqual(json.loads(self.path.read_text()), self.previous)
if __name__ == "__main__":
unittest.main()
+6 -4
View File
@@ -205,7 +205,7 @@ class PBSManagerTests(unittest.TestCase):
def test_password_sign_in_keeps_only_a_read_only_token(self):
calls = []
def request(server, method, endpoint, headers, form=None, rejected=""):
def request(server, method, endpoint, headers, form=None, rejected="", **kwargs):
calls.append((method, endpoint, form))
if endpoint == "/access/ticket":
return {"ticket": "PBS:root@pam:ticket", "CSRFPreventionToken": "csrf", "username": "root@pam"}
@@ -227,7 +227,7 @@ class PBSManagerTests(unittest.TestCase):
def test_password_sign_in_removes_its_token_when_the_role_cannot_be_granted(self):
calls = []
def request(server, method, endpoint, headers, form=None, rejected=""):
def request(server, method, endpoint, headers, form=None, rejected="", **kwargs):
calls.append((method, endpoint))
if endpoint == "/access/ticket":
return {"ticket": "t", "CSRFPreventionToken": "c"}
@@ -351,15 +351,17 @@ class TLSTests(unittest.TestCase):
def test_bad_pin_and_untrusted_ca_send_no_http_credentials(self):
for fingerprint in ('0' * 64, ''):
with self.subTest(fingerprint=fingerprint), self.assertRaises(pbs.PBSError):
with self.subTest(fingerprint=fingerprint), self.assertRaises(pbs.PBSError) as caught:
pbs._remote_get({**self.connection, "fingerprint": fingerprint}, '/status/datastore-usage')
self.assertEqual(caught.exception.code, "pbs_fingerprint_mismatch" if fingerprint else "pbs_connect_failed")
self.assertEqual(self.requests, [])
def test_bad_credentials_and_redirects_are_rejected(self):
for status in (401, 403, 302):
type(self).response_status = status
with self.subTest(status=status), self.assertRaises(pbs.PBSError):
with self.subTest(status=status), self.assertRaises(pbs.PBSError) as caught:
pbs._remote_get(self.connection, '/status/datastore-usage')
self.assertEqual(caught.exception.code, "pbs_token_rejected" if status in (401, 403) else "pbs_invalid_response")
if __name__ == '__main__':
+196
View File
@@ -0,0 +1,196 @@
"""PBS localization metadata must preserve routes, security and persistence."""
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import types
import unittest
from unittest import mock
from flask import Flask
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT / "AppImage/scripts"))
import auth_manager
import pbs_manager as pbs
REAL_REMOTE_REQUEST = pbs._remote_request
class PBSI18nTests(unittest.TestCase):
def setUp(self):
temp = tempfile.TemporaryDirectory(); self.addCleanup(temp.cleanup)
self.root = Path(temp.name)
for patch in [mock.patch.multiple(auth_manager, CONFIG_DIR=self.root, AUTH_CONFIG_FILE=self.root / "auth.json"),
mock.patch.object(pbs, "_remote_get", return_value=[]),
mock.patch.object(pbs, "_remote_request"),
mock.patch.object(pbs, "local_available", return_value=False)]:
patch.start(); self.addCleanup(patch.stop)
(self.root / "auth.json").write_text(json.dumps({"enabled": True, "password_hash": "fixture"}))
self.server = {"name": "Fixture", "url": "https://pbs.invalid:8007", "token_id": "audit@pbs!monitor", "token_secret": "fake-fixture-secret", "fingerprint": ""}
self.id = pbs.save_server(self.server)
pbs._remote_get.reset_mock(); pbs._status_cache.clear()
auth = types.SimpleNamespace(load_auth_config=auth_manager.load_auth_config, verify_token=lambda _: "fixture",
verify_token_full=lambda token: ("fixture", "full_admin" if token == "admin" else "read_only"))
spec = importlib.util.spec_from_file_location("fixture_pbs_jwt", ROOT / "AppImage/scripts/jwt_middleware.py")
middleware = importlib.util.module_from_spec(spec)
with mock.patch.dict(sys.modules, {"auth_manager": auth}): spec.loader.exec_module(middleware)
spec = importlib.util.spec_from_file_location("fixture_pbs_routes", ROOT / "AppImage/scripts/flask_pbs_routes.py")
routes = importlib.util.module_from_spec(spec)
with mock.patch.dict(sys.modules, {"jwt_middleware": middleware}): spec.loader.exec_module(routes)
app = Flask(__name__); app.register_blueprint(routes.pbs_bp); self.client = app.test_client()
def send(self, method="POST", path="/api/pbs/servers", data=None, role="admin"):
return self.client.open(path, method=method, json=data, headers={"Authorization": "Bearer " + role} if role else {})
def reject(self, data, message, code, path="/api/pbs/servers"):
before = (self.root / "pbs_servers.json").read_bytes()
with mock.patch.object(pbs, "_save_servers") as save:
response = self.send(path=path, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json, {"error": message, "code": code})
self.assertEqual(response.headers["Cache-Control"], "no-store")
save.assert_not_called(); pbs._remote_get.assert_not_called(); pbs._remote_request.assert_not_called()
self.assertEqual((self.root / "pbs_servers.json").read_bytes(), before)
def test_invalid_input_is_localizable_before_any_remote_or_save_effect(self):
cases = [
(None, "Invalid PBS server configuration.", "pbs_invalid_config"),
({**self.server, "name": []}, "Invalid PBS connection field.", "pbs_invalid_field"),
({**self.server, "url": "http://pbs.invalid"}, "Use an HTTPS PBS URL without embedded credentials, query or fragment.", "pbs_invalid_url"),
({**self.server, "name": ""}, "A server name of at most 128 characters is required.", "pbs_name_required"),
({**self.server, "token_id": "bad"}, "Enter a PBS API token ID in user@realm!token format.", "pbs_token_id_format"),
({**self.server, "token_secret": "bad\nsecret"}, "A valid API token secret is required.", "pbs_token_secret"),
({**self.server, "token_id": "človek@pbs!monitor"}, "Invalid API token ID.", "pbs_invalid_token_id"),
({**self.server, "fingerprint": "invalid"}, "The certificate fingerprint must be SHA256 (64 hex characters).", "pbs_invalid_fingerprint"),
({**self.server, "id": "../../bad"}, "Invalid PBS server ID.", "pbs_invalid_id"),
]
for data, message, code in cases:
with self.subTest(code=code): self.reject(data, message, code)
for data, message, code in [
({**self.server, "username": "missing-realm", "password": "fake"}, "Enter the PBS user as user@realm, for example root@pam.", "pbs_username"),
({**self.server, "username": "root@pam", "password": ""}, "Enter the password of the PBS user.", "pbs_password"),
]:
with self.subTest(code=code): self.reject(data, message, code, "/api/pbs/servers/login")
def test_authentication_and_scope_guards_precede_all_mutations(self):
before = (self.root / "pbs_servers.json").read_bytes()
save_patch = mock.patch.object(pbs, "_save_servers")
save = save_patch.start(); self.addCleanup(save_patch.stop)
for path, method in [("/api/pbs/servers", "POST"), ("/api/pbs/servers/login", "POST"), ("/api/pbs/servers/" + self.id, "DELETE")]:
for role, status in [(None, 401), ("reader", 403)]:
self.assertEqual(self.send(method, path, self.server, role).status_code, status)
(self.root / "auth.json").write_text(json.dumps({"enabled": False}))
for path in ("/api/pbs/servers", "/api/pbs/servers/login"):
response = self.send(path=path, data=self.server)
self.assertEqual(response.status_code, 409)
self.assertEqual(response.json, {"error": "Enable local authentication before saving remote PBS credentials.", "code": "pbs_auth_required"})
response = self.send("DELETE", "/api/pbs/servers/" + self.id)
self.assertEqual(response.status_code, 409)
self.assertEqual(response.json["code"], "pbs_auth_required")
pbs._remote_get.assert_not_called(); pbs._remote_request.assert_not_called()
save.assert_not_called()
self.assertEqual((self.root / "pbs_servers.json").read_bytes(), before)
def test_corrupt_configuration_missing_server_and_local_unavailable_keep_status(self):
response = self.send("DELETE", "/api/pbs/servers/absent")
self.assertEqual((response.status_code, response.json), (400, {"error": "PBS server not found.", "code": "pbs_not_found"}))
for identifier, code in [("absent", "pbs_not_found"), ("local", "pbs_local_unavailable")]:
response = self.send("GET", "/api/pbs/status?server=" + identifier)
self.assertEqual(response.status_code, 502); self.assertEqual(response.json["code"], code)
(self.root / "pbs_servers.json").write_text("invalid json")
response = self.send("GET", "/api/pbs/servers")
self.assertEqual((response.status_code, response.json), (500, {"error": "Could not read the PBS server configuration.", "code": "pbs_config_read"}))
pbs._remote_get.assert_not_called(); pbs._remote_request.assert_not_called()
def test_save_delete_failures_leave_previous_bytes_and_do_not_leak_details(self):
for path, method, data, code, message in [
("/api/pbs/servers", "POST", {**self.server, "id": self.id}, "pbs_save_failed", "Could not save PBS connection."),
("/api/pbs/servers/" + self.id, "DELETE", None, "pbs_remove_failed", "Could not remove PBS connection."),
]:
before = (self.root / "pbs_servers.json").read_bytes()
with self.subTest(method=method), mock.patch.object(pbs.os, "replace", side_effect=OSError("private filesystem detail")):
response = self.send(method, path, data)
self.assertEqual((response.status_code, response.json), (500, {"error": message, "code": code}))
self.assertEqual((self.root / "pbs_servers.json").read_bytes(), before)
def test_partial_status_preserves_legacy_error_null_and_cache_with_additive_metadata(self):
def remote(server, endpoint):
if endpoint == "/version": return {"version": "4.2.5"}
if endpoint == "/admin/verify": raise pbs.PBSError("Fixture access denied", code="pbs_token_rejected")
if endpoint == "/admin/prune": return {"invalid": "list"}
return []
with mock.patch.object(pbs, "_remote_get", side_effect=remote) as remote_call:
result = self.send("GET", "/api/pbs/status?server=" + self.id)
count = remote_call.call_count
self.assertEqual(self.send("GET", "/api/pbs/status?server=" + self.id).json, result.json)
self.assertEqual(remote_call.call_count, count)
self.assertEqual(result.status_code, 200)
self.assertIsNone(result.json["verify"]); self.assertIsNone(result.json["prune"])
self.assertEqual(result.json["sync"], [])
self.assertEqual(result.json["errors"]["verify"], "Fixture access denied")
self.assertEqual(result.json["error_details"]["verify"], {"code": "pbs_token_rejected"})
self.assertEqual(result.json["error_details"]["prune"], {"code": "pbs_invalid_list"})
def test_local_datastore_errors_and_command_errors_keep_legacy_messages(self):
with mock.patch.object(pbs, "_cli", return_value=[{"name": "offline", "path": "/fixture", "backing-device": "fixture"}]), \
mock.patch.object(pbs.os.path, "ismount", return_value=False), mock.patch.object(pbs.shutil, "disk_usage") as usage:
store = pbs._local_datastores()[0]
self.assertEqual(store["error"], "Datastore is offline, in maintenance or has no mounted backing device.")
self.assertEqual(store["error_code"], "pbs_datastore_offline"); usage.assert_not_called(); self.assertIsNone(store["total"])
with mock.patch.object(pbs.subprocess, "run", side_effect=OSError("fixture")):
for fn, args in [(pbs._cli, ("task", "list")), (pbs._local_api, ("/admin/sync",))]:
with self.subTest(args=args), self.assertRaises(pbs.PBSError) as caught: fn(*args)
self.assertEqual(caught.exception.code, "pbs_read_failed")
self.assertIn("Check its service and permissions.", str(caught.exception))
def test_remote_failure_codes_follow_the_actual_request_and_keep_english(self):
cases = [
(401, b'{}', "pbs_token_rejected", "PBS rejected the API token or its permissions. Check both the user and token ACLs."),
(302, b'{}', "pbs_invalid_response", "PBS returned an invalid response. Redirects are not followed."),
(200, b'{}', "pbs_invalid_json", "PBS returned an invalid JSON document."),
(200, b'invalid-json', "pbs_connect_failed", "Could not connect to PBS over verified HTTPS. Check the address, certificate and service."),
]
for status, body, code, message in cases:
connection = mock.Mock(); connection.getresponse.return_value.status = status
connection.getresponse.return_value.read.return_value = body
with self.subTest(code=code), mock.patch.object(pbs.http.client, "HTTPSConnection", return_value=connection), \
self.assertRaises(pbs.PBSError) as caught:
REAL_REMOTE_REQUEST(self.server, "GET", "/version", {})
self.assertEqual((str(caught.exception), caught.exception.problem()), (message, {"code": code}))
connection.close.assert_called_once()
for code in ("pbs_login_rejected", "pbs_token_create_denied", "pbs_audit_role_denied"):
connection = mock.Mock(); connection.getresponse.return_value.status = 403
connection.getresponse.return_value.read.return_value = b'{}'
with self.subTest(code=code), mock.patch.object(pbs.http.client, "HTTPSConnection", return_value=connection), self.assertRaises(pbs.PBSError) as caught:
REAL_REMOTE_REQUEST(self.server, "POST", "/access/ticket", {}, rejected="Original context", rejected_code=code)
self.assertEqual((str(caught.exception), caught.exception.code), ("Original context", code))
def test_connection_limit_and_invalid_datastore_response_do_not_save(self):
before = (self.root / "pbs_servers.json").read_bytes()
with mock.patch.object(pbs, "_save_servers") as save, mock.patch.object(pbs, "_load_servers", return_value=[{"id": str(i)} for i in range(32)]):
response = self.send(data=self.server)
self.assertEqual(response.json, {"error": "At most 32 PBS connections may be configured.", "code": "pbs_connection_limit", "params": {"count": 32}})
self.assertEqual(response.status_code, 400); save.assert_not_called()
pbs._remote_get.return_value = {}
response = self.send(data=self.server)
self.assertEqual(response.json["code"], "pbs_invalid_datastores")
self.assertEqual((self.root / "pbs_servers.json").read_bytes(), before)
def test_login_failures_keep_saved_state_and_cleanup_a_created_token(self):
before = (self.root / "pbs_servers.json").read_bytes()
login = {"name": "Fixture", "url": "https://pbs.invalid", "username": "root@pam", "password": "fake"}
for stage, code in [("ticket", "pbs_invalid_login"), ("token", "pbs_invalid_token"), ("acl", "pbs_audit_role_denied")]:
calls = []
def request(server, method, endpoint, headers, form=None, rejected="", rejected_code="pbs_token_rejected"):
calls.append((method, endpoint))
if endpoint == "/access/ticket": return None if stage == "ticket" else {"ticket": "fake", "CSRFPreventionToken": "fake"}
if method == "POST": return None if stage == "token" else {"tokenid": "audit@pbs!fixture", "value": "fake"}
if method == "PUT": raise pbs.PBSError(rejected, code=rejected_code)
pbs._remote_request.side_effect = request
response = self.send(path="/api/pbs/servers/login", data=login)
self.assertEqual(response.status_code, 400); self.assertEqual(response.json["code"], code)
self.assertEqual((self.root / "pbs_servers.json").read_bytes(), before)
self.assertEqual(sum(method == "DELETE" for method, _ in calls), 1 if stage == "acl" else 0)
if __name__ == "__main__": unittest.main()