ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes

OCI manager Apps
- App tab: containers installed from an OCI image are identified from their
  installation record; the application and image versions are shown and an
  update is detected by image digest; repository link; Refresh data.
- Updates tab for OCI containers: Update and Recreate run the same flow as the
  OCI menu in the Monitor terminal; the pre-update backup can be kept in a
  backup storage; scheduled image updates with an optional minimum age.
- Logs tab: console output of the application, kept on the host
  (lxc.console.logfile + logrotate) and followed live.
- The Proxmox console opens a shell (cmode: shell) when the image has one.
- A damaged image download is fetched again before failing.
- Multi-container applications open at their LAN address; volume mount
  points on block storage report their usage.

Monitor
- Proxmox notifications are delivered to a loopback-only HTTP listener when
  HTTPS is enabled, so they no longer fail certificate verification.
- Log persistence counts recurring patterns only; an ended burst is not
  reported as persistent and its warning clears on its own (#386).
- Proxmox notification config backups are deduplicated and capped at three.
- The update icon on the Apps page opens the container on its Updates tab.
- Version 1.2.6.2-beta and its release notes in every Monitor language.

Docs
- OCI manager Apps and Audit & Report rebuilt as per-page message files,
  with a new page for OCI containers in the Monitor.
- Seven pages fixed where rich-text tags were missing from t.rich.

Translations
- Spanish fixes across the OCI engine, the Monitor and the TUI menus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-25 21:51:12 +02:00
co-authored by Claude Opus 5.5
parent 386d33df6e
commit 4437a671d2
524 changed files with 14459 additions and 3841 deletions
+1 -1
View File
@@ -1 +1 @@
9216421e41d7927e188fd157adf2704d5f5f465819e19041cb619602637746be ProxMenux-1.2.6.1-beta.AppImage
b546a85dfe0fd2c5920042893a5a96ae007f962921f997183ebac8fb6697b53b ProxMenux-1.2.6.2-beta.AppImage
+25 -3
View File
@@ -595,9 +595,9 @@ function CardsGrid({
// `changeTab` switches the outer tab (dashboard-level) and
// `openLxcAppModal` tells VirtualMachines which guest to open and on
// which inner tab to land. Both fire in the same tick.
function openLxcModalOnAppTab(vmid: number) {
function openLxcModalOnAppTab(vmid: number, tab: "app" | "updates" = "app") {
window.dispatchEvent(new CustomEvent("changeTab", { detail: { tab: "vms" } }))
window.dispatchEvent(new CustomEvent("openLxcAppModal", { detail: { vmid } }))
window.dispatchEvent(new CustomEvent("openLxcAppModal", { detail: { vmid, tab } }))
}
// Same pattern for a QEMU guest: land on the modal's Status tab
@@ -637,6 +637,15 @@ function AppCard({
}
}
// The update icon leads to where the update is applied: the Updates
// tab of the container.
const goToUpdates = (e: React.MouseEvent | React.KeyboardEvent) => {
e.preventDefault()
e.stopPropagation()
if (link.vmid == null) return
openLxcModalOnAppTab(link.vmid, "updates")
}
const goToEditor = (e: React.MouseEvent | React.KeyboardEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -682,7 +691,20 @@ function AppCard({
<Pencil className="h-4 w-4" />
</button>
) : link.updateAvailable && (
<ArrowUpCircle className="h-5 w-5 text-purple-400 flex-shrink-0 self-start mt-0.5" aria-hidden="true" />
link.vmid != null && link.guestType !== "qemu" ? (
<button
type="button"
onClick={goToUpdates}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") goToUpdates(e) }}
className="rounded-full flex-shrink-0 self-start mt-0.5 text-purple-400 hover:text-purple-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label={t("apps.openUpdatesAria", { name: link.appName })}
title={t("apps.openUpdatesTitle")}
>
<ArrowUpCircle className="h-5 w-5" />
</button>
) : (
<ArrowUpCircle className="h-5 w-5 text-purple-400 flex-shrink-0 self-start mt-0.5" aria-hidden="true" />
)
)}
</div>
+1 -1
View File
@@ -629,7 +629,7 @@ export default function Hardware() {
const hasRealtimeData = (): boolean => {
if (!realtimeGPUData) return false
// Esto permite mostrar datos incluso cuando la GPU está inactiva (valores en 0 o null)
// Show the data even while the GPU is idle, when values are 0 or null
return realtimeGPUData.has_monitoring_tool === true
}
+205 -23
View File
@@ -43,7 +43,7 @@ import { useT } from "@/lib/i18n/provider"
// apps and casual "just want a link" registrations use this default.
type InstalledVia = "" | "dpkg" | "apk" | "file" | "binary" |
"python_dist" | "docker_label" | "docker_exec" |
"command" | "manual"
"command" | "manual" | "oci_image"
type GithubSource = "releases" | "tags"
interface PortEntry {
@@ -120,6 +120,10 @@ interface DetectedApp {
// auto-fill the Web Link editor when the user clicks "Register".
category?: string | null
tracking_suggestion?: TrackingSuggestion | null
// The scheme the application is served on, when it is known rather than
// guessed. A ProxMenux OCI install records it; Chromium answers on 3001
// over https, which no port heuristic can tell from 3000 over http.
scheme?: "http" | "https" | null
}
interface AppState {
@@ -129,8 +133,31 @@ interface AppState {
update_available: boolean | null
error: string | null
checked_at: string | null
// An application installed from an OCI image: the image it runs and the
// one its registry publishes today. The image decides whether there is an
// update; the version above only says which application it carries.
installed_digest?: string | null
latest_digest?: string | null
image_created?: string | null
latest_image_created?: string | null
image_reference?: string | null
image_repository?: string | null
}
// The repository of an OCI image, shown the way the repository of a tracked
// application is: owner/name for GitHub, the image name for Docker Hub.
export const ociRepoLabel = (url: string) =>
url.replace(/^https?:\/\/(www\.)?/, "")
.replace(/^github\.com\//, "")
.replace(/^hub\.docker\.com\/(r|_)\//, "")
.replace(/\/$/, "")
// "2026-09-24 · a3f21c08": an image as a reader can compare two of them.
export const ociImageLabel = (created?: string | null, digest?: string | null) =>
[created ? created.slice(0, 10) : null, digest ? digest.split(":").pop()!.slice(0, 8) : null]
.filter(Boolean)
.join(" · ")
interface AppEntry extends AppConfig {
id: string
state?: AppState
@@ -219,10 +246,25 @@ interface DockerWebLinkSuggestion {
logo_url?: string | null
}
// Identity of a container ProxMenux installed from an OCI image. It comes
// from the installation record rather than a probe, so it names the
// application even when the guest also ships something else — CT 152 runs
// Chromium and carries a docker client the probe reported instead.
interface OciInstance {
template_id: string | null
image_reference: string | null
repository: string | null
scheme: string | null
port: number | null
path: string | null
website: string | null
}
interface Suggestions {
ready?: boolean
name_suggestion: string | null
helper_slug: string | null
oci_instance?: OciInstance | null
port_suggestions: number[]
web_path_hint: string | null
tracking_suggestion?: TrackingSuggestion | null
@@ -400,6 +442,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const [loading, setLoading] = useState(!seed)
const [sidecar, setSidecar] = useState<SidecarResponse | null>(seed?.sidecar ?? null)
const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null)
const [adguardSetupAvailable, setAdguardSetupAvailable] = useState(false)
const [error, setError] = useState<string | null>(null)
const [searchingApplications, setSearchingApplications] = useState(false)
const [detectionNotice, setDetectionNotice] = useState<{ found: boolean; text: string } | null>(null)
@@ -589,14 +632,19 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const detectedList: DetectedApp[] = useMemo(() => {
if (!suggestions) return []
const out: DetectedApp[] = []
if (suggestions.helper_slug && suggestions.name_suggestion) {
// A ProxMenux OCI install has no helper slug — nothing was installed by a
// community script — so its template id identifies it instead.
const primarySlug = suggestions.helper_slug || suggestions.oci_instance?.template_id
if (primarySlug && suggestions.name_suggestion) {
out.push({
slug: suggestions.helper_slug,
slug: primarySlug,
name: suggestions.name_suggestion,
logo_url: suggestions.logo_url,
default_ports: suggestions.default_ports,
category: suggestions.category,
tracking_suggestion: suggestions.tracking_suggestion,
scheme: suggestions.oci_instance?.scheme === "https" ? "https"
: suggestions.oci_instance?.scheme === "http" ? "http" : null,
})
}
const seen = new Set(out.map((d) => d.slug))
@@ -639,6 +687,43 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const hiddenDetections = [...detectedList, ...(suggestions?.docker_workloads || [])]
.filter((d, index, items) => dismissedSlugs.has(d.slug) && items.findIndex(item => item.slug === d.slug) === index)
// A container installed from an OCI image holds exactly the application its
// record names, so there is nothing to search for and nothing else to add.
// What can go stale is what the record and the registry say, and this
// reads both again.
const isOciInstall = !!suggestions?.oci_instance
const isOciAdguard = suggestions?.oci_instance?.template_id === "image-adguard-home"
useEffect(() => {
if (!isOciAdguard) return
let cancelled = false
fetchApi<{ available: boolean }>(`/api/vms/${vmid}/apps/adguard-setup`)
.then((result) => { if (!cancelled) setAdguardSetupAvailable(result.available === true) })
.catch(() => { if (!cancelled) setAdguardSetupAvailable(false) })
return () => { cancelled = true }
}, [vmid, isOciAdguard])
const refreshOciData = async () => {
setSearchingApplications(true)
setDetectionNotice(null)
setError(null)
try {
const result: Suggestions = await fetchApi(`/api/vms/${vmid}/apps/suggestions`, {
method: "POST",
})
setSuggestions(result)
let next = sidecar
if (apps.length > 0) {
next = await fetchApi(`/api/vms/${vmid}/apps/check`, { method: "POST" })
setSidecar(next)
}
if (next) setLxcAppsCached(vmid, next, result)
onChange?.()
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.checkFailed"))
} finally {
setSearchingApplications(false)
}
}
const searchInstalledApplications = async () => {
const before = new Set([...visibleDetected, ...visibleWorkloads].map((item) => item.slug))
setSearchingApplications(true)
@@ -756,10 +841,16 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
// Docker endpoints come from the real published host-port mappings
// listed under Web links. Do not pre-save a catalog default such as
// 9000; the user explicitly chooses which workload links to add.
if (p.slug !== "docker" && p.default_ports?.length) {
seed.ports = p.default_ports.map((port) => ({
const suggestedPorts = isOciAdguard && p.slug === "image-adguard-home"
? [80, ...(adguardSetupAvailable ? [3000] : [])]
: p.default_ports || []
if (p.slug !== "docker" && suggestedPorts.length) {
seed.ports = suggestedPorts.map((port) => ({
port,
scheme: defaultSchemeFor(port),
...(isOciAdguard && port === 3000 ? { description: "Config" } : {}),
// A recorded scheme beats the port heuristic: getting this wrong
// hands the user a link that cannot connect.
scheme: p.scheme || defaultSchemeFor(port),
web_path: s?.web_path_hint || "",
// Auto-fill Categoría from helpers_cache.category_names[0]
// when the catalog entry carries one. User can still change
@@ -822,7 +913,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
setEditing({ appId: existing?.id || null, draft: seed })
setDetectorTest(null)
setError(null)
}, [suggestions, vmid, sidecar])
}, [suggestions, vmid, sidecar, isOciAdguard, adguardSetupAvailable])
const closeEditor = () => {
setEditing(null)
@@ -1686,10 +1777,26 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
<SelectItem value="docker_exec">{t("vmLxc.appEditor.methodDockerExec")}</SelectItem>
<SelectItem value="command">{t("vmLxc.appEditor.methodCommand")}</SelectItem>
<SelectItem value="manual">{t("vmLxc.appEditor.methodManual")}</SelectItem>
{/* Only meaningful on a container ProxMenux installed
from an image: the record it reads exists nowhere else. */}
{(method === "oci_image" || suggestions?.oci_instance) && (
<SelectItem value="oci_image">{t("vmLxc.appEditor.methodOciImage")}</SelectItem>
)}
</SelectContent>
</Select>
</div>
{method === "oci_image" && (
<div className="sm:col-span-2 text-xs text-muted-foreground leading-relaxed">
{t("vmLxc.appEditor.ociImageHelp")}
{suggestions?.oci_instance?.image_reference && (
<div className="mt-1">
<code className="text-foreground/80">{suggestions.oci_instance.image_reference}</code>
</div>
)}
</div>
)}
{isPackaged && (
<div>
<Label htmlFor="app-package">{t("vmLxc.appEditor.packageIdentifierLabel")}</Label>
@@ -1892,7 +1999,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
)}
{method && (() => {
{/* The registry of the image is the upstream of an OCI
install, and the record already names it. */}
{method && method !== "oci_image" && (() => {
// Upstream source selector — 3 methods (github,
// http_json, docker_hub). Legacy sidecars with a
// `repo` set but no `upstream_type` default to
@@ -2466,17 +2575,20 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)}
<div className="pt-1 flex flex-wrap justify-center gap-2">
<Button
onClick={searchInstalledApplications}
onClick={isOciInstall ? refreshOciData : searchInstalledApplications}
disabled={searchingApplications}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
{searchingApplications
? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
: <Search className="h-4 w-4 mr-1.5" />}
{searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
: isOciInstall ? <RefreshCw className="h-4 w-4 mr-1.5" /> : <Search className="h-4 w-4 mr-1.5" />}
{isOciInstall
? t(searchingApplications ? "vmLxc.appEditor.refreshingData" : "vmLxc.appEditor.refreshData")
: searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
</Button>
{!isOciInstall && (
<Button
onClick={openBrowseOrEditor}
className="bg-blue-500 hover:bg-blue-600 text-white"
@@ -2489,6 +2601,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</span>
)}
</Button>
)}
</div>
{detectionNotice && (
<p className={`text-xs text-center ${detectionNotice.found ? "text-emerald-400" : "text-muted-foreground"}`}>
@@ -2524,6 +2637,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
{app.installed_via === "apk" && app.package && <>apk · <code className="text-foreground/80">{app.package}</code></>}
{app.installed_via === "file" && app.file_path && <>file · <code className="text-foreground/80">{app.file_path}</code></>}
{app.installed_via === "binary" && app.binary_path && <>binary · <code className="text-foreground/80">{app.binary_path}</code></>}
{app.installed_via === "oci_image" && st?.image_reference && <>OCI · <code className="text-foreground/80 break-all">{st.image_reference}</code></>}
</div>
)}
{tracking && st?.checked_at && (
@@ -2535,6 +2649,17 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
stack below Checked, full-width so long repo
names wrap cleanly instead of competing with
the top-right on narrow screens. */}
{app.installed_via === "oci_image" && tracking && st?.image_repository && (
<a
href={st.image_repository}
target="_blank"
rel="noopener noreferrer"
className="md:hidden mt-1 text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1 min-w-0"
>
<span className="truncate">{ociRepoLabel(st.image_repository)}</span>
<ExternalLink className="h-3 w-3 flex-shrink-0" />
</a>
)}
{app.repo && tracking && (
<a
href={`https://github.com/${app.repo}`}
@@ -2551,6 +2676,17 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
{/* Desktop-only repo link: same row as the title on md+,
hidden on mobile where the stacked variant above
handles it. */}
{app.installed_via === "oci_image" && tracking && st?.image_repository && (
<a
href={st.image_repository}
target="_blank"
rel="noopener noreferrer"
className="hidden md:inline-flex text-xs text-muted-foreground hover:text-foreground items-center gap-1 flex-shrink-0 mt-1"
>
{ociRepoLabel(st.image_repository)}
<ExternalLink className="h-3 w-3" />
</a>
)}
{app.repo && tracking && (
<a
href={`https://github.com/${app.repo}`}
@@ -2565,6 +2701,41 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
{(() => {
// An application installed from an OCI image changes when its
// image does. Both are shown — the application version it
// carries and the image it runs — and the image decides.
if (app.installed_via === "oci_image") {
if (!tracking) return null
const newImage = st?.update_available === true
const appMoved = !!(st?.latest_version && st.latest_version !== st.installed_version)
return (
<div className="mb-3 grid gap-3 grid-cols-1 sm:grid-cols-2">
<div className="p-3 rounded-md bg-muted/40">
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.ociAppVersion")}</div>
<div className="text-lg font-semibold font-mono text-foreground break-all">
{st?.installed_version || "—"}
</div>
{appMoved && (
<div className="text-xs font-mono text-purple-400 mt-1 break-all">→ {st!.latest_version}</div>
)}
</div>
<div className="p-3 rounded-md bg-muted/40">
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.ociImage")}</div>
<div className="text-sm font-semibold font-mono text-foreground">
{ociImageLabel(st?.image_created, st?.installed_digest) || "—"}
</div>
{newImage ? (
<div className="text-sm font-semibold font-mono text-purple-400 mt-1 flex items-center gap-2">
{t("vmLxc.appEditor.ociNewImage")}: {ociImageLabel(st?.latest_image_created, st?.latest_digest)}
<ArrowUpCircle className="h-5 w-5 text-purple-400 flex-shrink-0" aria-label={t("vmLxc.appEditor.updateAvailableBadge")} />
</div>
) : st?.latest_digest ? (
<div className="text-xs text-muted-foreground mt-1">{t("vmLxc.appEditor.ociImageCurrent")}</div>
) : null}
</div>
</div>
)
}
// A delegated app has no upstream of its own; the version to
// compare against comes from the image it updates with.
const delegated = app.update_via === "docker"
@@ -2614,9 +2785,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
Logo is optional (per-port `logo_url`); when absent
the row indents naturally to align with the text.
If we can't resolve an IP for the CT we hide the row. */}
{app.ports && app.ports.length > 0 && (
{(isOciAdguard ? true : !!app.ports?.length) && (
<div className="mb-3 space-y-4">
{app.ports.map((p) => {
{(isOciAdguard
? [
{ port: 80, description: app.name, scheme: "http" as const },
...(adguardSetupAvailable ? [{ port: 3000, description: "Config", scheme: "http" as const }] : []),
]
: app.ports).map((p) => {
const url = buildWebUrl(ctIp, p.port, p.scheme, p.custom_url)
if (!url) return null
const label = p.description || app.name
@@ -2766,22 +2942,27 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
<Button
variant="outline"
size="sm"
onClick={searchInstalledApplications}
onClick={isOciInstall ? refreshOciData : searchInstalledApplications}
disabled={searchingApplications || editMode}
className="min-w-[7rem] sm:min-w-0 px-2.5 sm:px-3"
aria-label={searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
aria-label={isOciInstall
? t(searchingApplications ? "vmLxc.appEditor.refreshingData" : "vmLxc.appEditor.refreshData")
: searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
>
{searchingApplications
? <Loader2 className="h-4 w-4 sm:mr-1.5 animate-spin" />
: <Search className="h-4 w-4 sm:mr-1.5" />}
: isOciInstall ? <RefreshCw className="h-4 w-4 sm:mr-1.5" /> : <Search className="h-4 w-4 sm:mr-1.5" />}
<span className="hidden sm:inline">
{searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
{isOciInstall
? t(searchingApplications ? "vmLxc.appEditor.refreshingData" : "vmLxc.appEditor.refreshData")
: searchingApplications
? t("vmLxc.appEditor.searchingApplications")
: t("vmLxc.appEditor.searchApplications")}
</span>
</Button>
{!isOciInstall && (
<Button
variant="outline"
size="sm"
@@ -2800,6 +2981,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)}
</span>
</Button>
)}
<button
type="button"
onClick={() => setEditMode((v) => !v)}
+1 -2
View File
@@ -178,8 +178,7 @@ export function LxcTerminalModal({
// `cancelled` short-circuits the async init if the modal closes
// before the dynamic xterm import resolves. Without this, we'd
// construct a Terminal instance, attach it to a now-stale ref, and
// open a WebSocket that nobody listens to. Audit Tier 6 — useEffect
// con `import("xterm")` sin cancelación.
// open a WebSocket that nobody listens to.
let cancelled = false
// Small delay to ensure Dialog content is rendered
+2 -3
View File
@@ -665,9 +665,8 @@ function renderVertical(data: NetworkFlowData, labels: FlowLabels, unit: Network
const NIC_PATH_START_OFFSET = 46 // clears SUB_OFFSET_Y + text height
const NIC_VERTICAL_LEG = 56
const HOST_GAP_FROM_CONVERGE = 56
const GUEST_ROW_H = 100 // más separación vertical entre
// guests para que sub no toque
// el círculo del siguiente
const GUEST_ROW_H = 100 // enough vertical room that one guest's
// sub clears the next guest's circle
const BRIDGE_PITCH_PAD = 36
// Vertical positions of the label and the sub (rate) BELOW each
// node's circle. Both grew when the font went up to 12.5 px; this
+62 -89
View File
@@ -15,7 +15,7 @@ import { useT } from "../lib/i18n/provider"
import {
Bell, BellOff, Send, CheckCircle2, XCircle, Loader2,
AlertTriangle, Info, Settings2, Zap, Eye, EyeOff,
Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook,
Trash2, ChevronRight, TestTube2, Mail, Webhook,
Copy, Server, Shield, ExternalLink, RefreshCw, Download, Upload,
Cloud, Brain, Globe, MessageSquareText, Sparkles, Pencil, Save, RotateCcw, Lightbulb,
Moon, Newspaper
@@ -343,7 +343,6 @@ export function NotificationSettings() {
const [testing, setTesting] = useState<string | null>(null)
const [testResult, setTestResult] = useState<{ channel: string; success: boolean; message: string } | null>(null)
const [showHistory, setShowHistory] = useState(false)
const [showAdvanced, setShowAdvanced] = useState(false)
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
// Cleartext secrets cached only while the eye toggle is "on" for
// that field. Settings GET returns "************" for everything in
@@ -509,13 +508,6 @@ export function NotificationSettings() {
if (showHistory) loadHistory()
}, [showHistory, loadHistory])
// Auto-expand AI section when AI is enabled
useEffect(() => {
if (config.ai_enabled) {
setShowAdvanced(true)
}
}, [config.ai_enabled])
const updateConfig = (updater: (prev: NotificationConfig) => NotificationConfig) => {
setConfig(prev => {
const next = updater(prev)
@@ -919,13 +911,13 @@ export function NotificationSettings() {
return flat
}
const handleSave = async () => {
const saveConfig = async (nextConfig: NotificationConfig): Promise<boolean> => {
setSaving(true)
setSaveError(null)
try {
// If notifications are being disabled, clean up PVE webhook first
const wasEnabled = originalConfig.enabled
const isNowDisabled = !config.enabled
const isNowDisabled = !nextConfig.enabled
if (wasEnabled && isNowDisabled) {
try {
@@ -935,26 +927,33 @@ export function NotificationSettings() {
}
}
const payload = flattenConfig(config)
const payload = flattenConfig(nextConfig)
await fetchApi("/api/notifications/settings", {
method: "POST",
body: JSON.stringify(payload),
})
setOriginalConfig(config)
setConfig(nextConfig)
setOriginalConfig(nextConfig)
setHasChanges(false)
setEditMode(false)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
loadStatus()
return true
} catch (err) {
console.error("Failed to save notification settings:", err)
const msg = err instanceof Error ? err.message : t("settings.notifications.errors.saveFailed")
setSaveError(msg)
return false
} finally {
setSaving(false)
}
}
const handleSave = async () => {
await saveConfig(config)
}
const handleCancel = () => {
setConfig(originalConfig)
setHasChanges(false)
@@ -2226,74 +2225,45 @@ export function NotificationSettings() {
</p>
</div>
{/* ── Advanced: AI Enhancement ── */}
<div>
<div className="flex items-center justify-between py-1">
<button
className="flex items-center gap-2 text-sm text-foreground hover:bg-muted/60 rounded-md px-2 py-1.5 -mx-2 transition-colors"
onClick={() => setShowAdvanced(!showAdvanced)}
>
{showAdvanced ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
<Sparkles className="h-4 w-4 text-purple-400" />
<span className="font-medium">{t("settings.notifications.ai.title")}</span>
{config.ai_enabled ? (
<Badge variant="outline" className="text-[10px] border-purple-500/40 text-purple-400 ml-1">
{t("status.active")}
</Badge>
) : (
<Badge variant="outline" className="text-[10px] border-border text-muted-foreground ml-1">
{t("settings.notifications.ui.optional")}
</Badge>
)}
</button>
{showAdvanced && (
<div className="flex items-center gap-2">
{editMode ? (
<>
<button
className="h-6 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
onClick={handleCancel}
disabled={saving}
>
{t("actions.cancel")}
</button>
<button
className="h-6 px-2 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1"
onClick={handleSave}
disabled={saving || !hasChanges}
>
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : <CheckCircle2 className="h-3 w-3" />}
{t("actions.save")}
</button>
</>
) : (
<button
className="h-6 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1"
onClick={() => setEditMode(true)}
>
<Settings2 className="h-3 w-3" />
{t("actions.edit")}
</button>
)}
{/* ── AI Enhancement ── */}
<div className="space-y-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between py-1">
<div className="flex items-start gap-3">
<Sparkles className="h-5 w-5 text-purple-400 mt-0.5 shrink-0" />
<div>
<span className="text-sm font-medium">{t("settings.notifications.ai.title")}</span>
<p className="text-xs sm:text-sm text-muted-foreground">{t("settings.notifications.ai.enhancedMessagesDescription")}</p>
</div>
)}
</div>
{showAdvanced && (
<div className="space-y-4 mt-3 p-4 rounded-lg bg-muted/30 border border-border/50">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<Sparkles className="h-5 w-5 text-purple-400 mt-0.5 shrink-0" />
<div>
<span className="text-sm font-medium">{t("settings.notifications.ai.enhancedMessages")}</span>
<p className="text-xs sm:text-sm text-muted-foreground">{t("settings.notifications.ai.enhancedMessagesDescription")}</p>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2">
{editMode ? (
<>
<button
className="h-7 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
onClick={handleCancel}
disabled={saving}
>
{t("actions.cancel")}
</button>
<button
className="h-7 px-2 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1"
onClick={handleSave}
disabled={saving || !hasChanges}
>
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : <CheckCircle2 className="h-3 w-3" />}
{t("actions.save")}
</button>
</>
) : (
<button
className="h-7 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1"
onClick={() => setEditMode(true)}
>
<Settings2 className="h-3 w-3" />
{t("actions.edit")}
</button>
)}
<button
className={`relative w-9 h-[18px] rounded-full transition-colors ${
config.ai_enabled ? "bg-purple-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
} ${!editMode ? "opacity-60 cursor-not-allowed" : "cursor-pointer"}`}
@@ -2305,10 +2275,12 @@ export function NotificationSettings() {
<span className={`absolute top-[1px] left-[1px] h-4 w-4 rounded-full bg-white shadow transition-transform ${
config.ai_enabled ? "translate-x-[18px]" : "translate-x-0"
}`} />
</button>
</div>
</button>
</div>
</div>
{config.ai_enabled && (
{config.ai_enabled && (
<div className="space-y-4 p-4 rounded-lg bg-muted/30 border border-border/50">
<>
{/* Provider + Info button */}
<div className="space-y-2">
@@ -2661,10 +2633,12 @@ export function NotificationSettings() {
<Button
variant="outline"
size="sm"
onClick={() => {
updateConfig(p => ({ ...p, ai_custom_prompt: customPromptDraft }))
setEditingCustomPrompt(false)
handleSave()
onClick={async () => {
const nextConfig = { ...config, ai_custom_prompt: customPromptDraft }
if (await saveConfig(nextConfig)) {
setEditingCustomPrompt(false)
setCustomPromptDraft("")
}
}}
className="h-7 px-2 text-xs flex items-center gap-1 bg-blue-600 hover:bg-blue-700 text-white border-blue-600"
>
@@ -2714,8 +2688,8 @@ export function NotificationSettings() {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) {
const text = await file.text()
updateConfig(p => ({ ...p, ai_custom_prompt: text }))
handleSave()
const nextConfig = { ...config, ai_custom_prompt: text }
await saveConfig(nextConfig)
}
}
input.click()
@@ -2783,7 +2757,6 @@ export function NotificationSettings() {
)}
</div>
</>
)}
</div>
)}
</div>
@@ -0,0 +1,255 @@
"use client"
// Console output of a native OCI container: what the image's main process
// writes to stdout/stderr, kept on the host by liblxc. The first request
// brings the last N lines; while following, each poll sends back the byte
// offset and inode it holds and receives only what was appended. Nothing is
// cached — the backend reads the file on every request.
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { AlertCircle, Download, FileText, Loader2, Pause, Play, RefreshCw } from "lucide-react"
import { fetchApi } from "../lib/api-config"
import { useT } from "@/lib/i18n/provider"
interface ConsoleLogResponse {
ok: boolean
enabled: boolean
lines: string[]
offset: number
inode: number | null
reset?: boolean
head_truncated?: boolean
error?: string
}
const LINE_OPTIONS = [100, 500, 1000] as const
const POLL_MS = 2000
const lineTone = (line: string) => {
if (/\b(error|fatal|panic|critical|exception)\b/i.test(line)) return "text-red-400"
if (/\bwarn(ing)?\b/i.test(line)) return "text-amber-400"
return "text-foreground/90"
}
export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: boolean }) {
const t = useT()
const [lineCount, setLineCount] = useState<number>(500)
const [lines, setLines] = useState<string[]>([])
const [enabled, setEnabled] = useState(true)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [follow, setFollow] = useState(true)
const [filter, setFilter] = useState("")
const position = useRef<{ offset: number; inode: number | null } | null>(null)
const scroller = useRef<HTMLDivElement>(null)
const inFlight = useRef(false)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const r = await fetchApi<ConsoleLogResponse>(`/api/lxc/${vmid}/console-log?lines=${lineCount}`)
setEnabled(r.enabled)
setLines(r.lines || [])
position.current = { offset: r.offset, inode: r.inode }
if (r.error) setError(r.error)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
}, [vmid, lineCount])
const poll = useCallback(async () => {
const pos = position.current
if (!pos || inFlight.current) return
inFlight.current = true
try {
const inode = pos.inode === null ? "" : `&inode=${pos.inode}`
const r = await fetchApi<ConsoleLogResponse>(
`/api/lxc/${vmid}/console-log?lines=${lineCount}&offset=${pos.offset}${inode}`,
)
position.current = { offset: r.offset, inode: r.inode }
if (r.reset) {
setLines(r.lines || [])
} else if (r.lines?.length) {
setLines((prev) => {
const next = prev.concat(r.lines)
return next.length > lineCount ? next.slice(next.length - lineCount) : next
})
}
} catch {
// A failed poll leaves the view as it is; the next one retries.
} finally {
inFlight.current = false
}
}, [vmid, lineCount])
useEffect(() => {
load()
}, [load])
useEffect(() => {
if (!follow || !running || !enabled || loading) return
const timer = setInterval(() => {
if (document.visibilityState === "visible") poll()
}, POLL_MS)
poll()
return () => clearInterval(timer)
}, [follow, running, enabled, loading, poll])
const shown = useMemo(() => {
const needle = filter.trim().toLowerCase()
return needle ? lines.filter((l) => l.toLowerCase().includes(needle)) : lines
}, [lines, filter])
useEffect(() => {
if (follow && scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight
}, [shown, follow])
// Scrolling up to read stops following; the Follow button resumes it.
const onScroll = () => {
const el = scroller.current
if (!el || !follow) return
if (el.scrollHeight - el.scrollTop - el.clientHeight > 40) setFollow(false)
}
const download = () => {
const blob = new Blob([lines.join("\n") + "\n"], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `ct-${vmid}-console.log`
a.click()
URL.revokeObjectURL(url)
}
return (
<div className="h-full flex flex-col min-h-0">
<Card className="border border-border bg-card/50 flex flex-col flex-1 min-h-0">
<CardContent className="p-4 flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between mb-2 gap-2 flex-wrap shrink-0">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-sky-500/10">
<FileText className="h-4 w-4 text-sky-500" />
</div>
<h3 className="text-sm font-semibold text-foreground">{t("vmLxc.consoleLog.title")}</h3>
{enabled && shown.length > 0 && (
<Badge variant="secondary" className="text-xs h-5 ml-1">{shown.length}</Badge>
)}
</div>
{enabled && (
<div className="flex items-center gap-2 flex-wrap">
<Select value={String(lineCount)} onValueChange={(v) => setLineCount(Number(v))}>
<SelectTrigger className="h-7 w-auto text-xs gap-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LINE_OPTIONS.map((n) => (
<SelectItem key={n} value={String(n)} className="text-xs">
{t("vmLxc.consoleLog.lastLines", { count: n })}
</SelectItem>
))}
</SelectContent>
</Select>
{running && (
<Button
size="sm"
variant="outline"
className="h-7 text-xs gap-1"
onClick={() => setFollow((f) => !f)}
aria-pressed={follow}
>
{follow ? <Pause className="h-3 w-3" /> : <Play className="h-3 w-3" />}
<span>{follow ? t("vmLxc.consoleLog.pause") : t("vmLxc.consoleLog.follow")}</span>
</Button>
)}
<Button size="sm" variant="outline" className="h-7 text-xs gap-1" onClick={load} disabled={loading}>
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
<span>{t("vmLxc.consoleLog.refresh")}</span>
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-xs gap-1"
onClick={download}
disabled={lines.length === 0}
>
<Download className="h-3 w-3" />
<span>{t("vmLxc.consoleLog.download")}</span>
</Button>
</div>
)}
</div>
<p className="text-xs text-muted-foreground mb-3 shrink-0">
{t("vmLxc.consoleLog.description")}
{enabled && !running && <> {t("vmLxc.consoleLog.stopped")}</>}
</p>
{enabled && (
<Input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder={t("vmLxc.consoleLog.filter")}
className="h-8 text-xs mb-3 shrink-0"
/>
)}
{loading && lines.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-muted-foreground min-h-0">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
<span className="text-sm">{t("vmLxc.consoleLog.loading")}</span>
</div>
) : error ? (
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm shrink-0">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-red-500 shrink-0 mt-0.5" />
<div>
<p className="font-medium text-red-500 mb-1">{t("vmLxc.consoleLog.readFailed")}</p>
<p className="text-xs text-muted-foreground break-all">{error}</p>
</div>
</div>
</div>
) : !enabled ? (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 p-4 text-sm shrink-0">
<div className="flex items-start gap-2">
<FileText className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-2">
<p className="font-medium text-amber-500">{t("vmLxc.consoleLog.notAvailableTitle")}</p>
<p className="text-xs text-muted-foreground leading-relaxed">{t("vmLxc.consoleLog.notAvailableHint")}</p>
</div>
</div>
</div>
) : lines.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-sm text-muted-foreground min-h-0">
{t("vmLxc.consoleLog.empty")}
<div className="text-xs mt-1">{t("vmLxc.consoleLog.emptyHint")}</div>
</div>
) : shown.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground min-h-0">
{t("vmLxc.consoleLog.noMatches")}
</div>
) : (
<div
ref={scroller}
onScroll={onScroll}
className="rounded-md border border-border bg-background/50 flex-1 overflow-y-auto min-h-0"
>
<pre className="text-[11px] font-mono leading-snug whitespace-pre-wrap break-all p-3">
{shown.map((line, idx) => (
<div key={idx} className={lineTone(line)}>{line || " "}</div>
))}
</pre>
</div>
)}
</CardContent>
</Card>
</div>
)
}
+1 -2
View File
@@ -239,8 +239,7 @@ export function ProxmoxDashboard() {
fetchHealthInfoCount()
fetchUpdateStatus()
// En overview: cada 30 segundos para actualización frecuente del estado de salud
// En otras tabs: cada 60 segundos para reducir carga
// Overview polls health every 30 s; the other tabs every 60 s.
let interval: ReturnType<typeof setInterval> | null = null
let healthInterval: ReturnType<typeof setInterval> | null = null
if (activeTab === "overview") {
+23 -18
View File
@@ -3,7 +3,7 @@
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap, Sliders, Wrench, RefreshCw, Server, BellOff, Bell, Calendar, DatabaseBackup, Smartphone, Languages } from "lucide-react"
import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap, Sliders, Wrench, RefreshCw, Server, BellOff, Bell, Calendar, DatabaseBackup, Smartphone, Languages, Package, ScrollText, ArrowUpCircle } from "lucide-react"
import { Checkbox } from "./ui/checkbox"
import { useT } from "../lib/i18n/provider"
import { APP_VERSION } from "../lib/version"
@@ -305,34 +305,39 @@ export const CHANGELOG: Record<string, ReleaseNote> = {
// that haven't been curated by hand.
const CURRENT_VERSION_FEATURES = [
{
icon: <Activity className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.auditAssessment",
text: "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
icon: <Package className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.ociAppTab",
text: "OCI containers in the App tab — a container installed by OCI manager Apps is recognised from its installation record: the application and its image, a new image detected by digest, and a link to the image repository.",
},
{
icon: <Sliders className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.changeJournal",
text: "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
icon: <RefreshCw className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.ociUpdatesTab",
text: "Updates for OCI containers — Update and Recreate open the same flow as the OCI menu, the backup taken before updating can be kept in a backup storage, and the image can be updated on a schedule.",
},
{
icon: <Sparkles className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.auditReports",
text: "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
icon: <ScrollText className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.ociLogsTab",
text: "Logs tab for OCI containers — the console output of the application, kept on the host and followed live, with a filter and a download. The Proxmox console of these containers opens a shell.",
},
{
icon: <Calendar className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.auditPolicyBaseline",
text: "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
icon: <ArrowUpCircle className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.appsUpdateShortcut",
text: "The update icon on the Apps page opens the container straight on its Updates tab.",
},
{
icon: <Bell className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.groupedAppUpdates",
text: "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
key: "releaseNotes.currentFeatures.webhookHttps",
text: "Proxmox notifications reach the Monitor with HTTPS enabled — they are delivered on a local-only address and no longer fail with a certificate error.",
},
{
icon: <Shield className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.adminTokenScope",
text: "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n).",
icon: <Activity className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.persistentLogs",
text: "A burst of log errors that ended is no longer reported as persistent: a pattern has to keep appearing for 15 minutes, and its warning clears on its own (reported by @Joshua1264).",
},
{
icon: <HardDrive className="h-5 w-5" />,
key: "releaseNotes.currentFeatures.mountsLanAddress",
text: "Mount points on LVM-thin and other block storage show their usage, and multi-container applications open at their LAN address.",
},
]
@@ -287,8 +287,7 @@ const initMessage = {
// Snapshot the open-state at call time. After the dynamic xterm
// imports resolve, bail out if the modal has since been closed —
// otherwise we attach a Terminal to a stale ref and open a WS that
// nobody reads. Audit Tier 6 — useEffect con `import("xterm")` sin
// cancelación.
// nobody reads.
const wasOpenAtCall = isOpenRef.current
const [TerminalClass, FitAddonClass] = await Promise.all([
import("xterm").then((mod) => mod.Terminal),
+1 -1
View File
@@ -639,7 +639,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
}
const handleKeyButton = (key: string, e?: React.MouseEvent | React.TouchEvent) => {
// Prevenir comportamientos por defecto del navegador
// Stop the browser default for this key
if (e) {
e.preventDefault()
e.stopPropagation()
+407 -11
View File
@@ -21,7 +21,8 @@ import useSWR from "swr"
import { MetricsView } from "./metrics-dialog"
import { LxcTerminalModal } from "./lxc-terminal-modal"
import { ScriptTerminalModal } from "./script-terminal-modal"
import { LxcAppPanel, ThemeAwareLogo } from "./lxc-app-panel"
import { LxcAppPanel, ThemeAwareLogo, ociImageLabel } from "./lxc-app-panel"
import { OciConsoleLogPanel } from "./oci-console-log-panel"
import { AppUpdaterEditor, type AppUpdateMethod } from "./app-updater-editor"
import { formatStorage } from "../lib/utils"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
@@ -40,6 +41,17 @@ interface LxcPackageUpdate {
latest: string
security: boolean
}
// A container installed by OCI manager Apps, read from its record.
interface OciInstanceInfo {
oci_instance: boolean
console_log: boolean
stack: boolean
primary_vmid: number
members: number[]
host_directories: boolean
pending: boolean
}
interface LxcUpdateCheck {
available: boolean
count: number
@@ -114,6 +126,12 @@ interface LxcAppWatch {
error: string | null
checked_at: string | null
has_repo?: boolean
// OCI image of an application installed by OCI manager Apps.
image_reference?: string | null
image_created?: string | null
installed_digest?: string | null
latest_image_created?: string | null
latest_digest?: string | null
// Set for the synthetic entry that represents a ProxMenux-managed
// OCI app (Secure Gateway). The frontend renders it read-only +
// wires the Update action to /api/oci/installed/<id>/update.
@@ -951,7 +969,15 @@ export function VirtualMachines() {
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("default")
// Tab state for modal
const [activeModalTab, setActiveModalTab] = useState<"status" | "mounts" | "backups" | "app" | "updates" | "firewall">("status")
const [activeModalTab, setActiveModalTab] = useState<"status" | "mounts" | "backups" | "app" | "updates" | "firewall" | "logs">("status")
// OCI containers keep their console output on the host; the Logs tab is
// offered only when the backend finds that log for the open container.
const [consoleLogAvailable, setConsoleLogAvailable] = useState(false)
// Set when the open container was installed by OCI manager Apps: its
// Updates tab replaces the image instead of updating packages.
const [ociInstance, setOciInstance] = useState<OciInstanceInfo | null>(null)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" } | null>(null)
const [scheduleAckExternal, setScheduleAckExternal] = useState(false)
// 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
@@ -1094,9 +1120,10 @@ export function VirtualMachines() {
if (!vm) return
handleVMClick(vm)
// handleVMClick resets the inner tab to "status"; override to
// "app" in the same render tick — React batches these and the
// the requested tab ("app" by default, "updates" from the update
// icon) in the same render tick — React batches these and the
// last setActiveModalTab wins.
setActiveModalTab("app")
setActiveModalTab(detail.tab === "updates" ? "updates" : "app")
}
window.addEventListener("openLxcAppModal", handler as EventListener)
return () => window.removeEventListener("openLxcAppModal", handler as EventListener)
@@ -1249,6 +1276,8 @@ export function VirtualMachines() {
setFirewallLogs([])
setFirewallLogError(null)
setFirewallEnabled(true)
setConsoleLogAvailable(false)
setOciInstance(null)
// Prime UI from last-known payloads so a reopened guest never
// flashes "Loading…" — the backend and this cache both revalidate
@@ -1290,6 +1319,7 @@ export function VirtualMachines() {
// extends the same idea to the two tabs still on lazy-fetch.
if (vm.type === "lxc") {
fetchMountPoints(vm.vmid)
fetchOciInstance(vm.vmid)
// Shared cache dedup: if a hover already fired this, we share
// the same promise (no duplicate backend work). The App panel
// reads from the same cache, so switching to that tab either
@@ -1403,6 +1433,17 @@ export function VirtualMachines() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vmidsKey])
const fetchOciInstance = (vmid: number) =>
fetchApi<OciInstanceInfo>(`/api/lxc/${vmid}/oci-instance`)
.then((r) => {
setConsoleLogAvailable(!!r?.console_log)
setOciInstance(r?.oci_instance ? r : null)
})
.catch(() => {
setConsoleLogAvailable(false)
setOciInstance(null)
})
const fetchMountPoints = async (vmid: number) => {
// Two fetches in parallel:
// 1) STATIC — configured mp entries + PVE classification. Backed
@@ -1993,6 +2034,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
? s.targets.map((value: any) => String(value))
: ([...(legacyTarget !== "app" ? ["os"] : []), ...(legacyTarget !== "os" ? ["apps"] : [])]))
setScheduleReleaseDelayDays(Number.isInteger(Number(s.release_delay_days)) ? Number(s.release_delay_days) : 0)
setScheduleAckExternal(s.acknowledge_external_data === true)
if (s.backup !== undefined) setApplyBackup(!!s.backup)
if (s.backup_storage) setApplyBackupStorage(s.backup_storage)
if (s.restart !== undefined) setApplyRestart(!!s.restart)
@@ -2144,8 +2186,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
// leave scheduleConfigured=false and Save PUTs cron="" so the
// backend doesn't resurrect the schedule.
const cronToSave = scheduleEnabled || scheduleConfigured ? scheduleCron : ""
const hasOsTarget = scheduleTargets.includes("os")
const hasAppTarget = scheduleTargets.some((value) => value !== "os")
// A container installed by OCI manager Apps has one thing to update on
// a schedule: its image.
const targetsToSave = ociInstance ? ["oci_image"] : scheduleTargets
const hasOsTarget = targetsToSave.includes("os")
const hasAppTarget = targetsToSave.some((value) => value !== "os")
const derivedTarget: "os" | "app" | "both" = hasOsTarget && hasAppTarget ? "both" : hasOsTarget ? "os" : "app"
try {
await fetchApi(`/api/vms/${vmid}/schedule`, {
@@ -2155,11 +2200,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
enabled: scheduleEnabled,
cron: cronToSave,
target: derivedTarget,
targets: scheduleTargets,
targets: targetsToSave,
release_delay_days: scheduleReleaseDelayDays,
backup: applyBackup,
backup_storage: applyBackupStorage || selectedBackupStorage || "",
restart: applyRestart,
restart: ociInstance ? false : applyRestart,
acknowledge_external_data: ociInstance ? scheduleAckExternal : false,
}),
})
if (cronToSave.trim()) setScheduleConfigured(true)
@@ -2181,6 +2227,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
scheduleTarget: scheduleTarget,
scheduleTargets: [...scheduleTargets],
scheduleReleaseDelayDays: scheduleReleaseDelayDays,
scheduleAckExternal: scheduleAckExternal,
})
setOptionsEditMode(true)
}
@@ -2195,6 +2242,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
setScheduleTarget(optionsSnapshot.scheduleTarget)
setScheduleTargets(optionsSnapshot.scheduleTargets || ["os", "apps"])
setScheduleReleaseDelayDays(optionsSnapshot.scheduleReleaseDelayDays)
setScheduleAckExternal(!!optionsSnapshot.scheduleAckExternal)
}
setOptionsSnapshot(null)
setOptionsEditMode(false)
@@ -3761,6 +3809,24 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</Badge>
</button>
)}
{/* Logs tab — native OCI containers only: the console
output of the image's main process, read live from
the host on every open. */}
{selectedVM?.type === "lxc" && (consoleLogAvailable || selectedVM?.update_check?.is_oci_lxc) && (
<button
onClick={() => setActiveModalTab("logs")}
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "logs"
? "border-sky-500 text-sky-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<FileText className="h-4 w-4" />
<span className={activeModalTab === "logs" ? "" : "hidden sm:inline"}>
{t("vmLxc.tabs.logs")}
</span>
</button>
)}
<button
onClick={() => setActiveModalTab("backups")}
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
@@ -5130,8 +5196,299 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
)
})()}
{/* Branch 1 — OCI-image container */}
{!selectedVM.update_check?.managed_oci_app &&
{/* Branch 0b — container installed by OCI manager Apps. It is
updated by replacing its image, through the same flow as
the OCI menu; nothing of the package or app updaters of an
ordinary LXC applies. */}
{!selectedVM.update_check?.managed_oci_app && ociInstance && (() => {
const ociApp = (selectedVM.app_watches || []).find((a) => a.installed_via === "oci_image")
const hasUpdate = ociApp?.update_available === true
const upToDate = ociApp?.update_available === false
const installedLabel = ociApp ? ociImageLabel(ociApp.image_created, ociApp.installed_digest) : ""
const latestLabel = ociApp ? ociImageLabel(ociApp.latest_image_created, ociApp.latest_digest) : ""
const updateBtnCls = hasUpdate
? "bg-purple-600/15 hover:bg-purple-600/25 border border-purple-500/40 text-purple-300 hover:text-purple-200"
: "bg-green-500/10 hover:bg-green-500/20 border border-green-500/30 text-green-400 hover:text-green-300"
const neutralBtnCls = "border border-input bg-background text-foreground/80 hover:bg-accent hover:text-accent-foreground"
const storageForBackup = applyBackupStorage || selectedBackupStorage
return (
<>
<Card className="border border-border bg-card/50">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3 min-w-0">
<Package className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<h3 className="text-sm font-semibold text-foreground truncate">
{ociApp?.name || selectedVM.name}
</h3>
</div>
{ociApp?.checked_at && (
<div className="text-xs text-muted-foreground mb-3 leading-relaxed">
{t("vmLxc.updates.lastCheckedPrefix")} {new Date(ociApp.checked_at).toLocaleString()}
</div>
)}
{!ociApp ? (
<div className="text-sm text-muted-foreground">{t("vmLxc.ociUpdates.notTracked")}</div>
) : hasUpdate ? (
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.installedImage")} <code className="text-foreground/80">{installedLabel}</code></span>
</div>
<div className="flex items-center gap-2">
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.newImage", { image: latestLabel })}</span>
</div>
</div>
) : upToDate ? (
<div className="text-sm text-muted-foreground flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" />
<span>{t("vmLxc.updates.upToDateAtLabel")} <code className="text-foreground/80">{installedLabel}</code></span>
</div>
) : (
<div className="text-sm text-muted-foreground flex items-center gap-2">
<Package className="h-4 w-4 flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.installedImage")} <code className="text-foreground/80">{installedLabel || "—"}</code></span>
</div>
)}
{ociInstance.stack && (
<div className="text-xs text-muted-foreground mt-3 leading-relaxed">
{t("vmLxc.ociUpdates.stackNote", { primary: ociInstance.primary_vmid, count: ociInstance.members.length })}
</div>
)}
{ociInstance.pending && (
<div className="text-xs text-amber-400 mt-3 leading-relaxed">{t("vmLxc.ociUpdates.pendingNote")}</div>
)}
<div className="mt-4 pt-4 border-t border-border/50 flex flex-wrap justify-end gap-2">
{!ociInstance.stack && !ociInstance.pending && (
<Button
size="sm"
className={neutralBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recreate" })}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.recreate")}
</Button>
)}
<Button
size="sm"
className={updateBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "update" })}
>
{hasUpdate
? <ArrowUpCircle className="h-4 w-4 mr-1.5" />
: <RefreshCw className="h-4 w-4 mr-1.5" />}
{ociInstance.pending ? t("vmLxc.ociUpdates.recover") : t("vmLxc.ociUpdates.update")}
</Button>
</div>
</CardContent>
</Card>
{/* Options — the backup the update keeps and the
scheduled image update. */}
<Card className={optionsEditMode ? "border border-border bg-card" : "border border-border bg-card/50"}>
<CardContent className="p-4 space-y-4">
<h3 className="text-sm font-semibold text-foreground">{t("vmLxc.options.title")}</h3>
{!optionsEditMode ? (
<div className="text-sm flex items-center gap-2">
{applyBackup ? (
<CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" />
) : (
<div className="h-4 w-4 rounded-full border border-muted-foreground/40 flex-shrink-0" />
)}
<span>
{applyBackup
? <>{t("vmLxc.ociUpdates.keepBackup")} <span className="text-muted-foreground">{t("vmLxc.options.snapshotOn", { storage: storageForBackup || t("vmLxc.options.storageAuto") })}</span></>
: <span className="text-muted-foreground">{t("vmLxc.ociUpdates.noKeepBackup")}</span>}
</span>
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-2 text-sm">
<Checkbox
id="oci-keep-backup"
checked={applyBackup}
onCheckedChange={(v) => setApplyBackup(Boolean(v))}
className="mt-0.5"
/>
<Label htmlFor="oci-keep-backup" className="leading-tight cursor-pointer">
<span>{t("vmLxc.ociUpdates.keepBackup")}</span>
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.keepBackupHelp")}</div>
</Label>
</div>
{applyBackup && backupStorages.length > 0 && (
<div className="pl-6">
<Label className="text-xs text-muted-foreground">{t("vmLxc.options.backupStorage")}</Label>
<Select value={storageForBackup} onValueChange={setApplyBackupStorage}>
<SelectTrigger className="h-8 text-sm mt-1 max-w-xs">
<SelectValue placeholder={t("vmLxc.options.pickStorage")} />
</SelectTrigger>
<SelectContent>
{backupStorages.map((st) => (
<SelectItem key={st.storage} value={st.storage}>{st.storage}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
<div className="pt-4 border-t border-border/50 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">{t("vmLxc.scheduled.title")}</div>
{optionsEditMode && (
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.scheduleHelp")}</div>
)}
</div>
<Switch
checked={scheduleEnabled}
onCheckedChange={(v) => { if (optionsEditMode) setScheduleEnabled(v) }}
disabled={!optionsEditMode}
className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border"
/>
</div>
{!optionsEditMode && scheduleConfigured && (
<div className="text-sm space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<span className={"h-2 w-2 rounded-full flex-shrink-0 " + (scheduleEnabled ? "bg-green-500" : "bg-muted-foreground/40")} />
<span className={scheduleEnabled ? "" : "text-muted-foreground"}>
<span className="text-foreground/80">{t("vmLxc.scheduled.chipLabel")}</span> — {humanCron(scheduleCron)}
{!scheduleEnabled && <span className="text-muted-foreground"> {t("vmLxc.scheduled.disabledSuffix")}</span>}
</span>
</div>
{scheduleReleaseDelayDays > 0 && (
<div className="text-xs text-muted-foreground pl-4">
{t("vmLxc.scheduled.releaseDelaySummary", { days: scheduleReleaseDelayDays })}
</div>
)}
{renderScheduleRunDetails(true)}
</div>
)}
{!optionsEditMode && !scheduleConfigured && (
<div className="text-sm flex items-center gap-2 text-muted-foreground">
<div className="h-4 w-4 rounded-full border border-muted-foreground/40 flex-shrink-0" />
<span>{t("vmLxc.scheduled.notScheduled")}</span>
</div>
)}
{optionsEditMode && scheduleEnabled && (
<div className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.frequency")}</Label>
<Select
value={schedulePreset}
onValueChange={(v) => {
setSchedulePreset(v)
const preset = CRON_PRESETS.find((p) => p.value === v)
if (preset && preset.cron) setScheduleCron(preset.cron)
}}
>
<SelectTrigger className="h-8 text-sm mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
{CRON_PRESETS.map((p) => (
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.cronExpression")}</Label>
<Input
value={scheduleCron}
onChange={(e) => { setScheduleCron(e.target.value); setSchedulePreset("custom") }}
placeholder={t("vmLxc.scheduled.cronPlaceholder")}
className="h-8 text-sm mt-1 font-mono"
/>
</div>
</div>
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.releaseDelayLabel")}</Label>
<Select value={String(scheduleReleaseDelayDays)} onValueChange={(v) => setScheduleReleaseDelayDays(Number(v))}>
<SelectTrigger className="h-8 text-sm mt-1 max-w-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="0">{t("vmLxc.scheduled.releaseDelayNone")}</SelectItem>
{[1, 3, 7, 14].map((days) => (
<SelectItem key={days} value={String(days)}>{t("vmLxc.scheduled.releaseDelayDays", { days })}</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-[10px] text-muted-foreground mt-1 max-w-xl">{t("vmLxc.ociUpdates.releaseDelayHelp")}</div>
</div>
{ociInstance.host_directories && (
<div className="flex items-start gap-2 text-sm">
<Checkbox
id="oci-ack-external"
checked={scheduleAckExternal}
onCheckedChange={(v) => setScheduleAckExternal(Boolean(v))}
className="mt-0.5"
/>
<Label htmlFor="oci-ack-external" className="leading-tight cursor-pointer">
<span>{t("vmLxc.ociUpdates.ackExternal")}</span>
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.ackExternalHelp")}</div>
</Label>
</div>
)}
{renderScheduleRunDetails()}
</div>
)}
{optionsEditMode && scheduleConfigured && (
<div className="flex justify-end">
<button
type="button"
onClick={deleteScheduleFromOptions}
disabled={scheduleSaving}
className="h-8 px-3 text-xs rounded-md border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-red-400 transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
>
<Trash2 className="h-3.5 w-3.5" />
{t("vmLxc.scheduled.deleteButton")}
</button>
</div>
)}
{scheduleError && <div className="text-xs text-red-400">{scheduleError}</div>}
</div>
<div className="pt-4 border-t border-border/50 flex items-center justify-end gap-2">
{!optionsEditMode ? (
<button
type="button"
onClick={enterOptionsEdit}
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
>
<Settings2 className="h-3.5 w-3.5" />
{t("vmLxc.options.editButton")}
</button>
) : (
<>
<button
type="button"
onClick={cancelOptionsEdit}
disabled={scheduleSaving}
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
>
{t("vmLxc.options.cancelButton")}
</button>
<button
type="button"
onClick={saveOptionsEdit}
disabled={scheduleSaving || (scheduleEnabled && ociInstance.host_directories && !scheduleAckExternal)}
className="h-8 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-40 inline-flex items-center gap-1.5"
>
{scheduleSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
{t("vmLxc.options.saveButton")}
</button>
</>
)}
</div>
</CardContent>
</Card>
</>
)
})()}
{/* Branch 1 — OCI-image container not installed by
OCI manager Apps */}
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
selectedVM.update_check?.is_oci_lxc && (
<Card className="border border-border bg-card/50">
<CardContent className="p-4 space-y-2">
@@ -5164,7 +5521,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
Individual actions stay with their section. The
optional reusable bulk action is configured in its
own card immediately before Options. */}
{!selectedVM.update_check?.managed_oci_app &&
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
!selectedVM.update_check?.is_oci_lxc && (() => {
const uc = selectedVM.update_check
const osUpdateStatusKnown = !!uc && !uc.error
@@ -6957,6 +7314,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</Card>
</div>
)}
{activeModalTab === "logs" && selectedVM?.type === "lxc" && (
<OciConsoleLogPanel
key={selectedVM.vmid}
vmid={selectedVM.vmid}
running={selectedVM.status === "running"}
/>
)}
</div>
<div className="border-t border-border bg-background px-6 py-4 mt-auto shrink-0">
@@ -7390,6 +7755,37 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
}}
/>
)}
{/* Update or recreate an OCI instance: the same flow as OCI manager
Apps -> Manage installed OCI applications, for this container. */}
{ociAction && (
<ScriptTerminalModal
open={!!ociAction}
onClose={() => {
const vmid = ociAction.vmid
setOciAction(null)
// The image, the digest and the record changed: read them again.
fetchApi(`/api/vms/${vmid}/apps/check`, { method: "POST" })
.catch(() => undefined)
.finally(() => {
invalidateLxcApps(vmid)
vmModalCacheRef.current.mountPoints.delete(vmid)
fetchOciInstance(vmid)
fetchMountPoints(vmid)
mutate()
})
}}
scriptPath="/usr/local/share/proxmenux/scripts/oci/manage_instance.sh"
scriptName="oci_manage_instance"
title={ociAction.action === "update" ? t("vmLxc.ociUpdates.terminalTitleUpdate") : t("vmLxc.ociUpdates.terminalTitleRecreate")}
description={t("vmLxc.ociUpdates.terminalDescription")}
params={{
VMID: String(ociAction.vmid),
ACTION: ociAction.action,
KEEP_BACKUP: ociAction.action === "update" && applyBackup ? (applyBackupStorage || selectedBackupStorage || "") : "",
}}
/>
)}
</div>
)
}
+1 -1
View File
@@ -8,4 +8,4 @@
// 3. beta_version.txt ← bash pipeline (build_appimage.sh)
//
// Keep the three in sync on every bump.
export const APP_VERSION = "1.2.6.1-beta"
export const APP_VERSION = "1.2.6.2-beta"
+145 -136
View File
@@ -1487,7 +1487,6 @@
"containerNameLabel": "Containername",
"containerNamePlaceholder": "z. B. <Ihr-Container-Name>",
"ociLabelKeyLabel": "OCI-Labelschlüssel",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Pfad des Python-Interpreters",
"pythonInterpreterPlaceholder": "z. B. /opt/<your-app>/venv/bin/python",
"pipDistLabel": "Distributionsname (Pip-Paket)",
@@ -1534,11 +1533,8 @@
"portHttps": "https",
"portLogoLabel": "Logo-URL für diesen Link (optional)",
"portLogoPlaceholder": "z. B. https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
"portCategoryNone": "No category",
"portCategoryAddNew": "+ Add new category…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
"removePortTooltip": "Port entfernen",
"detectMethodDpkg": "dpkg ·",
"detectMethodApk": "apk ·",
@@ -1631,7 +1627,11 @@
"notifyUpstreamLabel": "Benachrichtigen Sie mich, wenn eine neue Upstream-Version verfügbar ist",
"notifyUpstreamHelp": "Sendet „app_update_available“ an die Kanäle, die in Einstellungen → Benachrichtigungen aktiviert sind. Deaktivieren Sie diese Option, wenn diese App auf Ihrer Box nicht aktualisiert werden kann.",
"excludeFromBadgeLabel": "Vom LXC-Aktualisierungszähler ausschließen",
"excludeFromBadgeHelp": "Zählen Sie diese App nicht im Abzeichen „Aggregate Updates“ auf der LXC-Listenkarte. Nützlich, wenn Sie absichtlich an eine bestimmte Version gebunden sind (Tracker-Anforderung, Kompatibilitätsstopp).Hat keinen Einfluss auf den eigenen Status der App-Registerkarte oder die ausgehende Benachrichtigung."
"excludeFromBadgeHelp": "Zählen Sie diese App nicht im Abzeichen „Aggregate Updates“ auf der LXC-Listenkarte. Nützlich, wenn Sie absichtlich an eine bestimmte Version gebunden sind (Tracker-Anforderung, Kompatibilitätsstopp).Hat keinen Einfluss auf den eigenen Status der App-Registerkarte oder die ausgehende Benachrichtigung.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"portCategoryPlaceholder": "Kategorie für das Apps-Dashboard (optional)",
"portCategoryCustomPlaceholder": "Geben Sie eine Kategorie ein und drücken Sie Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Benutzerdefinierte URL (z. B. https://vault.example.com) überschreibt IP:port"
},
"statusFilter": {
"ariaLabel": "Virtuelle Maschinen und Container filtern",
@@ -2515,10 +2515,10 @@
"scope": {
"label": "Token permissions",
"readOnly": "Read-only",
"readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.",
"fullAdmin": "Full admin",
"fullAdminHint": "Full control, like your own session.",
"fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations."
"readOnlyHint": "Lesen Sie Metriken und Status. Empfohlen für Dashboards und Integrationen.",
"fullAdminHint": "Volle Kontrolle, wie Ihre eigene Sitzung.",
"fullAdminWarning": "Ein Full-Admin-Token kann alles tun, was Sie können: den Host ausschalten oder neu starten, Updates ausführen und ein Terminal öffnen. Teilen Sie es nur mit vollständig vertrauenswürdigen Integrationen."
}
},
"firewall": {
@@ -3302,26 +3302,33 @@
"gotIt": "Habe es!",
"dontShowAgain": "Für diese Version nicht mehr anzeigen",
"currentFeatures": {
"appDetection": "Smarter app detection in the App tab: Docker is correctly promoted as the parent workload during cold start (Portainer/SearXNG no longer briefly show up as native apps), unregistered suggestions live in the startup cache, and 'Find applications' runs a fresh catalog-backed scan on demand.",
"dockerUpdates": "The Updates tab now covers Docker end-to-end: Docker Engine and per-image update tracking follow the same 24-hour rolling cycle as OS packages, with a 'Check now' action for on-demand digest comparison — no more waiting for the daily collector.",
"appCatalog": "New application detection catalog with over 380 tracked workloads, generated live from community-scripts across seven detector methods (file, binary, dpkg, apk, Python, Docker exec, Docker label). Primary and fallback detectors cover both new and historical LXC layouts.",
"pushover": "Pushover joins Telegram, Gotify, Discord, Email and Apprise as a native notification channel — user/API key, device and sound selectors, priority 0 for regular messages, optional priority 1 for CRITICAL events. Suggested by @benginx (#308).",
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
"auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
"changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"appDetection": "Intelligentere App-Erkennung im App-Tab: Docker wird korrekt gefördert, da der übergeordnete Workload während des Kaltstarts (Portainer/SearXNG wird nicht mehr kurzzeitig als native Apps angezeigt), nicht registrierte Vorschläge live im Start-Cache und \"Anwendungen finden\" führt bei Bedarf einen neuen kataloggestützten Scan aus.",
"dockerUpdates": "Die Registerkarte Updates umfasst jetzt Docker Ende-zu-Ende: Docker Engine und Per-Image-Update-Tracking folgen dem gleichen 24-Stunden-Rollenzyklus wie OS-Pakete, mit einer \"Check now\" -Aktion für den On-Demand-Digest-Vergleich - nicht mehr auf den täglichen Sammler warten.",
"appCatalog": "Neuer Anwendungserkennungskatalog mit über 380 verfolgten Workloads, der live aus Community-Scripts über sieben Detektormethoden generiert wurde (Datei, Binär, dpkg, apk, Python, Docker exec, Docker Label). Primär- und Fallbackdetektoren decken sowohl neue als auch historische LXC-Layouts ab.",
"pushover": "Pushover verbindet Telegram, Gotify, Discord, E-Mail und Apprise als nativen Benachrichtigungskanal - Benutzer / API-Schlüssel, Geräte- und Sound-Selektoren, Priorität 0 für reguläre Nachrichten, optionale Priorität 1 für kritische Ereignisse. Vorgeschlagen von @benginx (#308).",
"appsDashboard": "Neues Top-Level-Apps-Dashboard - ein einziger Launcher für jeden Weblink über den Knoten. LXC-registrierte Apps und benutzerdefiniertes Custom Web Links teilen sich das gleiche Raster mit Kategorieabzeichen, Suche und Ein-Klick-Deep-Links zurück zum Gastmodal.",
"lxcAppsUpdates": "Die App-Registerkarte in jedem LXC-Modal registriert installierte Apps, erfasst Weblinks und verfolgt Upstream-Versionen. Überarbeitete Updates Tab wendet OS-Pakete und App-Updates von einer einzigen Schaltfläche; Docker Engine und Per-Image-Tracking folgen dem gleichen 24-Stunden-Zyklus, mit einer \"Check now\" -Aktion auf Anfrage.",
"multilingual": "Der Monitor spricht jetzt 8 Sprachen: Englisch, Spanisch, Deutsch, Französisch, Italienisch, Portugiesisch, Schwedisch und Slowakisch. Großer Dank an @vaso73 für den Bau des i18n-Gerüsts, das dies ermöglicht hat.",
"nvidiaMultiGpu": "Der NVIDIA-Treiber-Lebenszyklus bewegt sich in den Besitz pro BDF, so dass ein Multi-GPU-Host eine Karte an einen VM übergeben und den anderen auf dem Host oder in LXCs betriebsbereit halten kann, sowie einen Kernel + Branch + GPU-fähige Versionsauswahl (# 298).",
"aiCustomEndpoint": "AI Assistant benutzerdefinierte OpenAI-Endpunkte – LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute und jeder selbst gehostete Proxy auf privaten IPs, Loopback- oder Docker-Netzwerken werden beim Laden des Modellkatalogs erkannt. Der Dropdown zeigt den Fehler des Servers (oder den zugrunde liegenden Netzwerkgrund) direkt unter der Schaltfläche Laden (#325, berichtet von @jorgeffonte).",
"secureGatewayArch": "Secure Gateway-Assistent - der Alpine Template-Download, die lokale Template-Auswahl und pct create passen alle zur realen Architektur des Hosts, so dass x86 64-Hosts amd64-Container und arm64-Hosts arm64-Container erhalten (#324, berichtet von @N0X4DD0).",
"atomicNotifications": "Benachrichtigungsereignisse reservieren ihren Deduplizierungs-Fingerabdruck atomar vor der KI-Verarbeitung und Kanalzustellung, so dass gleichzeitige Sammler, Abschlussrückrufe oder parallele Monitorprozesse dasselbe Ereignis nicht zweimal senden können. Die Reservierung wird freigegeben, wenn kein Kanal erfolgreich ist, wobei Wiederholungen beibehalten werden.",
"borgSshPort": "Borg-Remoteziel – der Add Borg-Zieldialog und die Shell TUI akzeptieren einen benutzerdefinierten SSH-Port. BORG RSH, der Autoschlüssel-Installationsfluss und die Kapazitätssonde ehren dies. Vollständig rückwärtskompatibel mit vorhandenen Einträgen, die ohne expliziten Port erstellt wurden (vorgeschlagen von @songochain in Diskussion #236).",
"githubToken": "Einstellungen → GitHub API akzeptiert ein optionales persönliches Zugriffstoken für die Freigabe und Tag-Prüfung, wenn das anonyme Kontingent erschöpft ist. Das Token wird im Ruhezustand verschlüsselt und niemals an den Browser zurückgegeben; der Rate-Limit-Fehler wird in jede Monitorsprache übersetzt (vorgeschlagen von @SystemIdleProcess in Diskussion #306).",
"replicationContext": "Native Proxmox Replikationsfehlermeldungen lösen die Replikationsauftrags-ID, die betroffene VM/LXC ID und den Gastnamen; der genaue Fehlerblock aus Proxmox bleibt als Grund erhalten, und jeder Replikationsauftrag wird unabhängig dedupliziert (von Ale R. berichtet).",
"auditAssessment": "Audit & Report - eine neue Seite, die den Knoten dokumentiert und bewertet. Die Bewertung führt einen Katalog von Prüfungen über Sicherheit, Backups, Kapazität, Speicher, Netzwerk, Hardware, Gäste und System durch, wobei jeder Befund als kritisch, Warnung, Beobachtung oder konform eingestuft wird und angegeben wird, was er gelesen hat, anstatt ihn zu beurteilen.",
"changeJournal": "Änderungsjournal - eine Änderungsansicht, in der genau aufgeführt ist, was ProxMenux auf diesem Host geändert hat: jede Konfigurationsdatei, jedes Paket und jeder Dienst, die es berührt hat, mit dem vorherigen Status von jedem, dedupliziert zu einer aktuellen Zustandsansicht, die den Host so zeigt, wie er jetzt steht, anstatt ein Protokoll von jedem Lauf.",
"auditReports": "Berichte - ein vollständiges Audit sowie eine fokussierte Sicherheitsüberprüfung, Backup Assurance und Capacity & Wear-Berichte, jede Öffnung auf einem eigenen Haltungskopf und ein druckbares Inventar; alle exportieren in PDF. Eine Sicherheitsbewertung fragt vor dem Ausführen von Lynis, damit ein Lauf schnell bleibt.",
"auditPolicyBaseline": "Policy und Baseline - Deklarieren Sie, was der Host sein soll (Backup-Anforderungen, Firewall, Root SSH-Login), damit die Ergebnisse dagegen bewertet werden, markieren Sie einen Lauf als Referenz und sehen Sie, was sich seitdem geändert hat, wobei neue, gelöste und akzeptierte Ergebnisse auseinandergehalten werden.",
"groupedAppUpdates": "Gruppierte Anwendungsaktualisierungsbenachrichtigungen — eine vollständige Nachricht pro Scan statt einer pro Anwendung, gruppiert nach LXC mit den installierten und verfügbaren Versionen.",
"adminTokenScope": "Administrativer Umfang: Das Öffnen eines Monitor-Terminals und das Deaktivieren der Authentifizierung erfordern jetzt ein Full-Admin-Token, so dass ein schreibgeschütztes API-Token, das an eine Überwachungsintegration ausgegeben wird, schreibgeschützt bleibt (berichtet von @f3rs3n).",
"ociAppTab": "OCI-Container in der App-Registerkarte — ein von OCI manager Apps installierter Container wird anhand seines Installationsdatensatzes erkannt: Anwendung und Image, ein neues Image per Digest erkannt und ein Link zum Image-Repository.",
"ociUpdatesTab": "Updates für OCI-Container — Aktualisieren und Neu erstellen öffnen denselben Ablauf wie das OCI-Menü, das vor dem Update erstellte Backup kann in einem Backup-Speicher behalten werden und das Image lässt sich zeitgesteuert aktualisieren.",
"ociLogsTab": "Logs-Registerkarte für OCI-Container — die Konsolenausgabe der Anwendung, auf dem Host gespeichert und live verfolgt, mit Filter und Download. Die Proxmox-Konsole dieser Container öffnet eine Shell.",
"appsUpdateShortcut": "Das Update-Symbol auf der Apps-Seite öffnet den Container direkt auf seiner Registerkarte Updates.",
"webhookHttps": "Proxmox-Benachrichtigungen erreichen den Monitor auch mit aktiviertem HTTPS — sie werden über eine nur lokale Adresse zugestellt und scheitern nicht mehr an einem Zertifikatsfehler.",
"persistentLogs": "Eine beendete Häufung von Log-Fehlern wird nicht mehr als dauerhaft gemeldet: Ein Muster muss 15 Minuten lang weiter auftreten, und seine Warnung schließt sich von selbst (gemeldet von @Joshua1264).",
"mountsLanAddress": "Mountpunkte auf LVM-thin und anderem Blockspeicher zeigen ihre Belegung, und Multi-Container-Anwendungen öffnen sich unter ihrer LAN-Adresse."
}
},
"network": {
@@ -5027,23 +5034,20 @@
"uncategorized": "Uncategorized",
"openAriaLabel": "Open {name} in a new tab",
"openGuestAriaLabel": "{name} öffnen ({type} {id})",
"emptyTitle": "No apps with a web link yet.",
"emptyHint": "Register a Web Link for any app (App tab of a CT) to see it here.",
"customLinkAdd": "Add link",
"editModeToggle": "Edit",
"editModeDone": "Done",
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Benutzerdefinierten Link {name} bearbeiten",
"openUpdatesAria": "Registerkarte Updates von {name} öffnen",
"openUpdatesTitle": "Update verfügbar — Registerkarte Updates öffnen",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
"customLinkLogo": "Logo URL (optional)",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkCategory": "Category (optional)",
"customLinkBinding": "Bound to",
"customLinkBindingNone": "Not bound to any guest",
"customLinkBindingHelp": "Bind this link to a VM or CT to add its ID + name to the card and jump to the guest with one click.",
"customLinkSave": "Save",
"customLinkCreate": "Create",
"customLinkCancel": "Cancel",
@@ -5079,7 +5083,12 @@
"remoteAccessVpn": "Remote Access & VPN",
"webserversProxies": "Webservers & Proxies",
"zigbeeZwaveMatter": "ZigBee, Z-Wave & Matter"
}
},
"emptyTitle": "Noch keine Apps mit einem Weblink.",
"emptyHint": "Registrieren Sie einen Weblink für eine beliebige App (App-Tab eines CT), um ihn hier zu sehen.",
"customLinkLogoPlaceholder": "z. B. https://example.com/logo.webp",
"customLinkBindingNone": "Nicht an einen Gast gebunden",
"customLinkBindingHelp": "Binden Sie diesen Link an eine VM oder CT, um die ID + den Namen der Karte hinzuzufügen und mit einem Klick zum Gast zu springen."
},
"audit": {
"presentation": {
@@ -5234,13 +5243,10 @@
"loading": "Loading assessment…",
"run": "Run assessment",
"running": "Assessing…",
"neverRun": "This host has not been assessed yet.",
"lastRun": "Last assessed on {when}",
"stale": "{days} days ago",
"unverifiedChecks": "Nicht erhoben: {checks}. Jede nennt in ihrer Evidenz, was sie nicht lesen konnte.",
"noFindings": "No findings match the current filter.",
"affectedCount": "{count} affected",
"acceptedNotice": "{count} accepted risk(s) recorded on this host.",
"reviewOn": "Überprüfen am: {when}",
"reviewDue": "Zur Überprüfung fällig — die Entscheidung gilt weiter",
"reviewDueNotice": "{count} akzeptierte Entscheidung(en) stehen zur Überprüfung an.",
@@ -5272,7 +5278,7 @@
"sources": "Quellen und Erfassungszeitpunkte"
},
"errors": {
"runFailed": "The assessment could not be started."
"runFailed": "Mit der Bewertung konnte nicht begonnen werden."
},
"checks": {
"backup": {
@@ -5285,7 +5291,7 @@
"uncovered": "{count} von {total} Gästen werden von keinem aktivierten Sicherungsauftrag ausgewählt",
"excludedData": "{count} Datenträger- oder Mountpoint-Ausschlüsse prüfen",
"uncoveredExpected": "{required} Gäste, für die eine Sicherung erklärt wurde, wählt kein aktivierter Auftrag aus",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
},
"nextStep": {
"noJobs": "Erstellen Sie einen Backup-Job auf diesem Knoten und wählen Sie die Gäste mit Daten aus, die Sie nicht von Hand neu erstellen möchten. Ein Job, der existiert, aber deaktiviert ist, wählt nichts aus.",
@@ -5300,10 +5306,10 @@
"rationale": "Alter: vergangene Zeit seit der letzten gespeicherten Sicherung. Verwendeter Grenzwert: das Referenzalter, mit dem diese Sicherung verglichen wird.",
"summary": {
"recent": "Alle {total} Gast-/Zielprüfungen erfüllen die angegebene Altersrichtlinie",
"stale": "{count} of {total} guests have no backup from the last 30 days",
"noBackups": "No stored backup matches a guest on this node",
"attention": "{count} von {total} Gast-/Zielprüfungen erfordern eine Überprüfung",
"evaluationFailed": "The check could not be evaluated"
"stale": "{count} von {total}-Gästen haben kein Backup der letzten 30 Tage",
"noBackups": "Kein gespeichertes Backup passt zu einem Gast auf diesem Knoten",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
},
"nextStep": {
"stale": "Finden Sie heraus, warum der Job aufhörte, Kopien für diese Gäste zu produzieren: Möglicherweise wurde er deaktiviert, sein Zeitplan wird möglicherweise nie ausgelöst oder seine Läufe können fehlschlagen. Der Run Results Check berichtet, wie jeder Job zuletzt beendet wurde.",
@@ -5317,10 +5323,10 @@
"rationale": "Die Aufbewahrung, wie Proxmox sie auflöst: Einstellung des Auftrags, dann des Speichers, dann der Knotenstandard. Aufbewahrung, die ein Sicherungsserver anwendet, ist von diesem Knoten aus nicht lesbar.",
"summary": {
"allDefined": "Alle {total} Aufträge lösen eine Aufbewahrungseinstellung auf",
"missing": "{count} of {total} enabled job(s) declare no retention",
"notDeclared": "{count} von {total} Aufträgen behalten jede Kopie: für sie ist keine Aufbewahrung erklärt",
"onServer": "{count} von {total} Aufträgen schreiben auf einen Sicherungsserver, der sie mit eigenen Aufträgen bereinigt",
"evaluationFailed": "The check could not be evaluated"
"missing": "{count} von {total}-fähigen Job(s) erklären keine Aufbewahrung",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
},
"nextStep": {
"missing": "Legen Sie eine Aufbewahrung für diese Jobs oder für den Speicher fest, an den sie schreiben. Proxmox nimmt zuerst die Einstellung des Jobs, dann den Speicher und dann den Node Default an.",
@@ -5383,36 +5389,36 @@
"summary": {
"none": "Nichts hat einen Neustart angefordert",
"pending": "{count} Elemente sind installiert und warten auf einen Neustart",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"enterprise_repo_without_subscription": {
"title": "Enterprise repository",
"rationale": "Verweise auf `enterprise.proxmox.com` in `/etc/apt/sources.list` und `sources.list.d`, gegenüber dem Status aus `pvesubscription get`.",
"summary": {
"notEnabled": "The enterprise repository is not enabled",
"subscribed": "The enterprise repository is backed by a subscription",
"unsubscribed": "The enterprise repository is enabled without an active subscription",
"evaluationFailed": "The check could not be evaluated"
"notEnabled": "Das Enterprise Repository ist nicht aktiviert",
"subscribed": "Das Enterprise-Repository wird durch ein Abonnement gesichert",
"unsubscribed": "Das Enterprise-Repository ist ohne aktives Abonnement aktiviert",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"memory_overcommit": {
"title": "Memory allocation",
"rationale": "Die `memory`-Obergrenze jeder Gastkonfiguration gegenüber MemTotal, laufende Gäste getrennt gezählt. Container verbrauchen bis zu dieser Grenze; virtuelle Maschinen ohne Ballooning reservieren sie.",
"summary": {
"withinRatio": "Guests are allocated {percent}% of host memory",
"aboveRatio": "Guests are allocated {percent}% of host memory",
"evaluationFailed": "The check could not be evaluated"
"withinRatio": "Gäste erhalten {percent}% des Host-Speichers",
"aboveRatio": "Gäste erhalten {percent}% des Host-Speichers",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"time_synchronisation": {
"title": "Time synchronisation",
"rationale": "NTP und NTPSynchronized, wie `timedatectl` sie meldet. Clusterzugehörigkeit, Zertifikatsprüfung und Protokollreihenfolge hängen von übereinstimmenden Uhren ab. Ein anderer Mechanismus kann die Uhr führen.",
"summary": {
"synchronised": "The clock is synchronised with a time source",
"disabled": "Time synchronisation is disabled",
"notSynchronised": "Time synchronisation is enabled but the clock is not synchronised",
"evaluationFailed": "The check could not be evaluated"
"synchronised": "Die Uhr wird mit einer Zeitquelle synchronisiert",
"notSynchronised": "Zeitsynchronisation ist aktiviert, aber die Uhr ist nicht synchronisiert",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"kernel_current": {
@@ -5420,21 +5426,21 @@
"rationale": "Der laufende Kernel gegenüber dem, den der Host als Nächstes starten würde, wie `proxmox-boot-tool` ihn meldet. Ein lediglich installierter neuerer Kernel kann bewusst zurückgehalten sein; ein Unterschied nach einem Neustart bedeutet einen Start, der nicht griff.",
"summary": {
"current": "Der laufende Kernel {version} ist der, den der Host als Nächstes starten würde",
"newerAvailable": "The host runs {running} while {newest} is installed",
"newerSelected": "Der Host führt {running} aus und würde beim nächsten Neustart {selected} starten",
"wouldDowngrade": "Der Host führt {running} aus, würde beim nächsten Neustart aber das ältere {selected} starten",
"bootTargetUnknown": "Der Host führt {version} aus; der für den nächsten Start gewählte Kernel war nicht lesbar",
"evaluationFailed": "The check could not be evaluated"
"newerAvailable": "Der Host führt {running} aus, während {newest} installiert ist",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"security_updates": {
"title": "Security updates",
"rationale": "Ausstehende Pakete, deren Quelle ein Sicherheits-Repository ist, aus einem simulierten `apt-get upgrade`. Die Zahl gibt wieder, was apt meldet, nicht die Schwere dessen, was jedes Paket behebt.",
"summary": {
"none": "No package updates are pending",
"noSecurity": "{total} update(s) pending, none from a security repository",
"pending": "{count} of {total} pending update(s) come from a security repository",
"evaluationFailed": "The check could not be evaluated"
"none": "Keine Paket-Updates stehen aus",
"noSecurity": "{total}-Update(s) ausstehend, keine aus einem Sicherheitsrepository",
"pending": "{count} von {total} ausstehende Updates stammen aus einem Sicherheitsrepository",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"journal_size": {
@@ -5442,10 +5448,10 @@
"rationale": "Das Journal auf der Platte gegenüber der für es geltenden Obergrenze: SystemMaxUse, sofern gesetzt, sonst der journald-Standard von einem Zehntel des Dateisystems, auf dem es liegt.",
"summary": {
"bounded": "Das Journal belegt {size} und bleibt unter seiner wirksamen Obergrenze",
"large": "The journal holds {size} on disk",
"nearCap": "Das Journal belegt {size} und liegt bei {percent}% seiner wirksamen Obergrenze",
"capUnknown": "Das Journal belegt {size}; die wirksame Obergrenze war nicht zu ermitteln",
"evaluationFailed": "The check could not be evaluated"
"large": "Das Journal hält {size} auf der Festplatte",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"swap_configured": {
@@ -5454,7 +5460,7 @@
"summary": {
"active": "{size} of swap is active",
"none": "No swap area is active",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"filesystem_capacity": {
@@ -5500,7 +5506,7 @@
"summary": {
"synchronised": "Die {total} Bootpartitionen tragen dieselben Kernel",
"attention": "{count} von {total} Bootpartitionen bedürfen der Prüfung",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
},
"rationale": "Die EFI-Systempartitionen proxmox-boot-tool-Berichte und die Kernel, die jeder trägt. proxmox-boot-tool-Status kann EFI-Systempartitionen vorübergehend bereitstellen; Es wird kein Bootversuch unternommen."
},
@@ -5510,7 +5516,7 @@
"summary": {
"allRunning": "Die {total} wesentlichen Dienste sind aktiv und keine Unit ist fehlgeschlagen",
"attention": "{count} Feststellung(en) unter den Units",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"ha_state": {
@@ -5519,7 +5525,7 @@
"summary": {
"managed": "Die {total} verwalteten Dienste sind auf {nodes} Knoten in einem gesetzten Zustand",
"attention": "{count} Feststellung(en) zu {total} verwalteten Diensten",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
}
},
@@ -5528,27 +5534,27 @@
"title": "Container privileges",
"rationale": "Die Einstellung `unprivileged` jeder Containerkonfiguration. Ihr Fehlen bedeutet, dass der Container den Benutzernamensraum des Hosts teilt, was manche Arbeitslasten benötigen.",
"summary": {
"allUnprivileged": "All {total} containers are unprivileged",
"privileged": "{count} of {total} containers run privileged",
"evaluationFailed": "The check could not be evaluated"
"allUnprivileged": "Alle {total} Container sind unprivilegiert",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"qemu_without_agent": {
"title": "Guest agent on virtual machines",
"rationale": "Die Einstellung `agent` in jeder Konfiguration einer virtuellen Maschine. Die Einstellung besagt, dass der Agent deklariert ist, nicht dass er antwortet.",
"summary": {
"allHaveAgent": "All {total} virtual machines declare the guest agent",
"missingAgent": "{count} of {total} virtual machines do not declare the guest agent",
"evaluationFailed": "The check could not be evaluated"
}
"allHaveAgent": "Alle virtuellen {total}-Maschinen erklären den Gastagenten",
"missingAgent": "{count} von virtuellen {total} Maschinen deklarieren nicht den Gastagenten",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
},
"title": "Gastagent auf virtuellen Maschinen"
},
"autostart": {
"title": "Automatic start",
"rationale": "Die Einstellung `onboot` jedes Gastes, ohne Vorlagen und von HA verwaltete Gäste. Ob ein Gast von selbst zurückkehren soll, ergibt sich aus der erklärten Richtlinie.",
"summary": {
"allAutostart": "All {total} guests start with the host",
"notAutostart": "{count} of {total} guests do not start with the host",
"evaluationFailed": "The check could not be evaluated"
"allAutostart": "Alle {total} Gäste beginnen mit dem Gastgeber",
"notAutostart": "{count} der {total} Gäste starten nicht mit dem Gastgeber",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"stuck_snapshots": {
@@ -5556,28 +5562,28 @@
"rationale": "Zustand und Alter der Snapshots sowie aktive Aufgaben. Ein kürzlicher oder undatierter Vorgang gilt nicht als unterbrochen.",
"summary": {
"noSnapshots": "No guest holds snapshots",
"allComplete": "The {total} snapshot(s) are complete",
"stuck": "{count} of {total} snapshot(s) were left mid-operation",
"evaluationFailed": "The check could not be evaluated"
"allComplete": "Die {total} Snapshots sind vollständig",
"stuck": "{count} von {total} Snapshot(s) wurden mitten in Betrieb gelassen",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"cpu_host_type": {
"title": "Virtual CPU model",
"rationale": "Der Wert `cpu` jeder virtuellen Maschine. `host` gibt den Funktionsumfang des physischen Prozessors weiter, was die Knoten einschränkt, auf die der Gast migrieren kann. Unverträglichkeit mit einem bestimmten Ziel wird hier nicht ermittelt.",
"summary": {
"none": "None of the {total} virtual machines is pinned to the host processor",
"pinned": "{count} of {total} virtual machines are pinned to the host processor",
"evaluationFailed": "The check could not be evaluated"
"none": "Keine der virtuellen {total}-Maschinen ist an den Host-Prozessor gebunden",
"pinned": "{count} von virtuellen {total}-Maschinen werden an den Host-Prozessor angeheftet",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"replication_state": {
"title": "Replication",
"rationale": "Replikationsaufträge aus der API: Fehlerzahl, letzter Fehler, letzte Synchronisierung und der Kalender, den jeder Auftrag angibt. Pausierte Aufträge werden als solche ausgewiesen.",
"summary": {
"healthy": "The {total} replication job(s) report no error",
"failing": "{count} of {total} replication job(s) report an error",
"statusUnavailable": "Replication jobs are defined but their status could not be read",
"evaluationFailed": "The check could not be evaluated"
"healthy": "Die {total} Replikationsjob(s) melden keinen Fehler",
"failing": "{count} von {total} Replikationsjob(s) melden einen Fehler",
"statusUnavailable": "Replikationsjobs sind definiert, aber ihr Status konnte nicht gelesen werden",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
}
},
@@ -5589,39 +5595,39 @@
"bothEnabled": "Die Firewall-Aktivierung ist auf Datacenter- und Knotenebene konfiguriert",
"datacenterOff": "Die Firewall ist auf Datacenter-Ebene deaktiviert; Knotenregeln werden daher nicht angewendet",
"nodeOff": "Die Firewall ist auf Datacenter-Ebene aktiviert, aber nicht auf diesem Knoten",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"lynis_warnings": {
"title": "Lynis warnings",
"rationale": "Warnungen der letzten Lynis-Prüfung, jeweils mit Test-Kennung, und das Alter dieser Prüfung. Eine Prüfung läuft nur, wenn Lynis installiert ist und kein vollständiger Bericht vorliegt. Vorschläge sind nicht enthalten.",
"summary": {
"none": "The last Lynis audit recorded no warnings",
"found": "The last Lynis audit recorded {count} warning(s)",
"incomplete": "The Lynis report is incomplete",
"evaluationFailed": "The check could not be evaluated",
"noneStale": "Die letzte Lynis-Prüfung verzeichnete keine Warnungen, und ihr Bericht ist {days} Tag(e) alt",
"foundStale": "Die letzte Lynis-Prüfung verzeichnete {count} Warnung(en), und ihr Bericht ist {days} Tag(e) alt"
"foundStale": "Die letzte Lynis-Prüfung verzeichnete {count} Warnung(en), und ihr Bericht ist {days} Tag(e) alt",
"none": "Das letzte Lynis-Audit verzeichnete keine Warnungen",
"found": "Das letzte Lynis-Audit hat {count}-Warnungen aufgezeichnet",
"incomplete": "Der Bericht Lynis ist unvollständig",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"certificate_expiry": {
"title": "Certificate validity",
"rationale": "Das Ablaufdatum des Zertifikats, das pveproxy aus /etc/pve/local ausliefert. Ein eigenes Zertifikat hat Vorrang vor dem von Proxmox erzeugten.",
"summary": {
"valid": "The certificate is valid for {days} more day(s)",
"expiring": "The certificate expires in {days} day(s)",
"expired": "The certificate expired {days} day(s) ago",
"evaluationFailed": "The check could not be evaluated"
"valid": "Das Zertifikat gilt für {days} weitere Tage",
"expiring": "Das Zertifikat läuft in {days} Tagen ab",
"expired": "Das Zertifikat ist vor {days} Tagen abgelaufen",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"ssh_root_login": {
"title": "SSH root access",
"rationale": "PermitRootLogin in der effektiven `sshd -T`-Konfiguration samt den kombinierten Authentifizierungsmethoden. Proxmox liefert `yes` aus, was ein Passwort zulässt.",
"summary": {
"password": "Root may sign in over SSH with a password",
"keyOnly": "Root may sign in over SSH with a key only",
"denied": "Root may not sign in over SSH",
"evaluationFailed": "The check could not be evaluated"
"password": "Root kann sich über SSH mit einem Passwort anmelden",
"keyOnly": "Root kann sich über SSH nur mit einem Schlüssel anmelden",
"denied": "Root kann sich nicht über SSH anmelden",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
}
},
@@ -5630,9 +5636,9 @@
"title": "Volume assignment",
"rationale": "Gastvolumes auf lokalem Speicher gegenüber den Verweisen in aktuellen, ausstehenden und Snapshot-Konfigurationen. Sicherungen, ISOs und Vorlagen bleiben außerhalb des Vergleichs. Ein Volume ohne Verweis ist ein Kandidat zur Prüfung.",
"summary": {
"none": "No orphaned volumes were found",
"found": "{count} Volumes ohne Verweis in den geprüften Konfigurationen",
"evaluationFailed": "The check could not be evaluated"
"none": "Es wurden keine verwaisten Volumen gefunden",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"zfs_arc_max": {
@@ -5640,21 +5646,21 @@
"rationale": "Effektive Werte von c_min, c_max und ARC-Größe, der geladene Modulparameter und die persistenten Einstellungen in /etc/modprobe.d. Ein konfigurierter Wert von null wählt den Modulstandard; der ARC ist eine Obergrenze, und der belegte Speicher ist rückgewinnbar.",
"summary": {
"bounded": "Die ARC-Grenze liegt bei {percent}% des Hostspeichers, und der belegte Speicher ist rückgewinnbar",
"high": "The ARC may use {percent}% of host memory",
"unset": "The ARC has no explicit limit set",
"conflicting": "{count} ARC-Einstellungen widersprechen einander",
"pending": "Eine persistente ARC-Einstellung weicht vom Wert des laufenden Moduls ab",
"evaluationFailed": "The check could not be evaluated"
"high": "Der ARC kann {percent}% des Hostspeichers verwenden",
"unset": "Der ARC hat kein explizites Limit",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"zfs_scrub_age": {
"title": "ZFS scrub",
"rationale": "Der letzte abgeschlossene Scrub, den `zpool status` je Pool meldet. Ein Resilver ist kein Scrub. Ein kürzlich erstellter Pool hatte noch keine Gelegenheit dazu.",
"summary": {
"recent": "The {total} pool(s) were scrubbed within the last 35 days",
"overdue": "{count} of {total} pool(s) have not been scrubbed in 35 days",
"neverScrubbed": "{count} pool(s) record no scrub",
"evaluationFailed": "The check could not be evaluated"
"recent": "Die {total}-Pools wurden innerhalb der letzten 35 Tage gewaschen",
"overdue": "{count} von {total} Pool(s) wurden in 35 Tagen nicht geschrubbt",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"thin_pool_overprovisioning": {
@@ -5664,7 +5670,7 @@
"withinRatio": "Die {total} Thin-Pools liegen unter den angewendeten Prüfgrenzen",
"aboveRatio": "{count} von {total} Thin-Pools vergeben mehr Kapazität, als sie besitzen",
"pressure": "{pressure} von {total} Thin-Pools füllen ihre Daten oder Metadaten fast aus",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"connected_storage": {
@@ -5673,7 +5679,7 @@
"summary": {
"available": "PVE meldet alle {total} Speicher als verfügbar; interne Remote-Komponenten und Schreibzugriff wurden nicht geprüft",
"attention": "{count} von {total} Speichern müssen geprüft werden",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"pool_integrity": {
@@ -5691,7 +5697,7 @@
"summary": {
"healthy": "Ceph meldet HEALTH_OK",
"degraded": "Ceph meldet {state}, mit {count} benannten Prüfung(en)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"array_integrity": {
@@ -5700,7 +5706,7 @@
"summary": {
"intact": "Die {total} Arrays und Maps halten ihre Redundanz",
"degraded": "{count} von {total} Arrays oder Maps fehlt sie",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
}
},
@@ -5712,7 +5718,7 @@
"withinLife": "Die {total} Datenträgermessungen liegen unter der Fünfjahresschwelle",
"pastLife": "{count} von {total} Datenträgermessungen liegen über fünf Betriebsjahren",
"noReadings": "Keine Festplatte meldet verwertbare SMART-Werte ({skipped} ohne Messwerte)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"disk_errors": {
@@ -5729,23 +5735,22 @@
"title": "Bond members",
"rationale": "Der MII-Status jedes Bond-Mitglieds aus /proc/net/bonding und wie viele Verbindungen bleiben. In active-backup meldet sich ein Reservemitglied als aktiv und überträgt nichts.",
"summary": {
"allUp": "All members of the {total} bond(s) are up",
"membersDown": "{count} bond member(s) are not up",
"evaluationFailed": "The check could not be evaluated"
"allUp": "Alle Mitglieder der {total} Bond (s) sind up",
"membersDown": "{count} Bond Mitglied (s) sind nicht oben",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
},
"bridge_without_ports": {
"title": "Bridge ports",
"rationale": "Die Portkonfiguration jeder Bridge. Eine Bridge ohne physischen Port bedient ein internes oder geroutetes Netz.",
"summary": {
"allConnected": "The {total} bridge(s) carry a port",
"isolated": "{count} of {total} bridge(s) carry no port",
"evaluationFailed": "The check could not be evaluated"
"allConnected": "Die {total} Bridge(s) tragen einen Port",
"evaluationFailed": "Die Überprüfung konnte nicht ausgewertet werden"
}
}
}
},
"summaryFallback": "The check could not be evaluated",
"acceptRisk": {
"action": "Risiko akzeptieren",
"revoke": "Wieder aktivieren",
@@ -5779,17 +5784,15 @@
"OK": "OK"
},
"viewSwitch": {
"ariaLabel": "Switch between assessment and inventory",
"assessment": "Assessment",
"inventory": "Inventory",
"policy": "Richtlinie",
"changes": "Änderungen"
"changes": "Änderungen",
"ariaLabel": "Wechsel zwischen Assessment und Inventar"
},
"inventory": {
"loading": "Loading inventory…",
"failed": "The inventory could not be composed.",
"collectedAt": "Composed on {when}",
"unavailable": "Not read in this inventory",
"unresolved": "path not resolved",
"noUplink": "no uplink",
"identity": "Node identity",
@@ -5832,12 +5835,10 @@
"protection": "Backup",
"passthrough": "Passthrough",
"noBackup": "No backup",
"noBackupDetail": "No enabled backup job selects this guest.",
"applications": "Applications",
"versionUnknown": "version not detected",
"passthroughTitle": "PCI passthrough",
"iommuGroup": "IOMMU group {group}",
"sharedGroup": "{count} more device(s) in the same group",
"proxmenux": "ProxMenux optimizations",
"latency": "Netzwerklatenz",
"subscriptionStatus": {
@@ -5848,7 +5849,11 @@
"suspended": "Ausgesetzt",
"new": "Aktivierung ausstehend",
"unknown": "Unbekannt"
}
},
"failed": "Das Inventar konnte nicht zusammengestellt werden.",
"unavailable": "Nicht in diesem Inventar lesen",
"noBackupDetail": "Kein aktivierter Backup-Job wählt diesen Gast aus.",
"sharedGroup": "{count} weitere Geräte in derselben Gruppe"
},
"profile": {
"label": "Report",
@@ -5871,7 +5876,6 @@
"area": "Area",
"inventoryAnnex": "Inventory annex",
"scopeTitle": "Scope of this report",
"scopeBody": "This report describes the Proxmox VE node named above, as observed from the node itself at the time stated. It does not cover the interior of the guests beyond what they declare, network equipment outside the host, physical infrastructure, or any dependency not visible from this node. Findings marked as not determined were not measured and are not evidence of absence.",
"building": "Bericht wird zusammengestellt…",
"node": "Knoten",
"profile": "Profil",
@@ -6020,11 +6024,12 @@
"structureSubtitle": "Wie {node} aufgebaut und konfiguriert ist",
"postureTitle": "Posture",
"posture": {
"security": "Host exposure and access.",
"backup": "Guest protection and whether it is real.",
"capacity": "Room to grow and the wear on the disks."
"security": "Host Exposition und Zugang.",
"backup": "Gästeschutz und ob es real ist.",
"capacity": "Raum zum Wachsen und der Verschleiß an den Scheiben."
},
"scopeReadOnly": "Die Bewertung überprüft die Hosteinstellungen und den Zustand. Es kann Berichte und Protokolle schreiben; Boot-Statusprüfungen können EFI-Systempartitionen vorübergehend bereitstellen."
"scopeReadOnly": "Die Bewertung überprüft die Hosteinstellungen und den Zustand. Es kann Berichte und Protokolle schreiben; Boot-Statusprüfungen können EFI-Systempartitionen vorübergehend bereitstellen.",
"scopeBody": "Dieser Bericht beschreibt den oben genannten Proxmox VE-Knoten, wie er vom Knoten selbst zum angegebenen Zeitpunkt beobachtet wurde. Es umfasst nicht das Innere der Gäste über das hinaus, was sie deklarieren, Netzwerkausrüstung außerhalb des Hosts, physische Infrastruktur oder jede Abhängigkeit, die von diesem Knoten nicht sichtbar ist. Befunde, die als nicht bestimmt markiert wurden, wurden nicht gemessen und sind kein Hinweis auf Abwesenheit."
},
"results": "Ergebnisse",
"classifications": {
@@ -6178,15 +6183,19 @@
"notApplicableScope": "Im geprüften Umfang gibt es nichts, worauf diese Prüfung zutrifft.",
"lynis": {
"title": "Run Lynis",
"bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.",
"bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.",
"withLynis": "Run with Lynis",
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
"cancel": "Cancel",
"bodyNotRun": "Lynis ist installiert, wurde aber noch nicht ausgeführt. Die Ausführung vervollständigt nun die Sicherheitsüberprüfung, aber der Prozess kann einige Minuten dauern.",
"bodyStale": "Der Lynis-Bericht ist {days} Tage alt. Sie können es jetzt ausführen, um die Daten zu aktualisieren (es dauert etwas länger) oder mit dem vorhandenen Bericht fortzufahren."
},
"readOnlyNotice": "Die Bewertung überprüft die Hosteinstellungen und den Zustand. Es kann Berichte und Protokolle schreiben; Boot-Statusprüfungen können EFI-Systempartitionen vorübergehend bereitstellen.",
"noActionNeeded": "Nichts zu tun. Dieser Check hat herausgefunden, was er erwartet.",
"couldNotEvaluate": "Diese Überprüfung konnte nicht ausgewertet werden, so dass sie nichts meldet. Die Beweise dokumentieren, was sie nicht lesen konnten."
"couldNotEvaluate": "Diese Überprüfung konnte nicht ausgewertet werden, so dass sie nichts meldet. Die Beweise dokumentieren, was sie nicht lesen konnten.",
"neverRun": "Dieser Gastgeber wurde noch nicht bewertet.",
"noFindings": "Keine Ergebnisse stimmen mit dem aktuellen Filter überein.",
"acceptedNotice": "{count} akzeptierte Risiken, die auf diesem Host aufgezeichnet wurden.",
"summaryFallback": "Die Überprüfung konnte nicht ausgewertet werden"
},
"runtime": {
"notifications": {
+60 -4
View File
@@ -1090,7 +1090,8 @@
"backups": "Backups",
"updates": "Updates",
"firewall": "Firewall",
"app": "App"
"app": "App",
"logs": "Logs"
},
"actions": {
"start": "Start",
@@ -1190,6 +1191,44 @@
"empty": "No firewall events recorded yet.",
"emptyHint": "Rules with log: info or higher will populate this view as packets arrive."
},
"consoleLog": {
"title": "Console output",
"description": "Standard output and error of the image's main process.",
"stopped": "The container is stopped; the log keeps the output of its previous runs.",
"lastLines": "Last {count} lines",
"follow": "Follow",
"pause": "Pause",
"refresh": "Refresh",
"download": "Download",
"filter": "Filter lines",
"loading": "Loading console output...",
"empty": "No output recorded yet.",
"emptyHint": "Lines appear here as the container writes to its console.",
"noMatches": "No lines match the filter.",
"readFailed": "Failed to read the console log",
"notAvailableTitle": "This container has no console log",
"notAvailableHint": "The console log is set up when ProxMenux installs the container from its OCI image."
},
"ociUpdates": {
"notTracked": "Registering the application in the App tab follows its image here.",
"installedImage": "Installed image",
"newImage": "New image {image} available",
"stackNote": "Part of a multi-container application (main CT {primary}, {count} containers): the update covers all of them.",
"pendingNote": "An operation on this container was interrupted; Recover opens its recovery.",
"update": "Update",
"recreate": "Recreate",
"recover": "Recover",
"terminalTitleUpdate": "Update OCI application",
"terminalTitleRecreate": "Recreate OCI application",
"terminalDescription": "The same flow as OCI manager Apps → Manage installed OCI applications.",
"keepBackup": "Keep the backup taken before updating",
"noKeepBackup": "The backup taken before updating is not kept",
"keepBackupHelp": "Every update backs up the container before replacing its image, to restore it if the update fails. With this option that backup is kept in the chosen storage; on Proxmox Backup Server a backup is written before the update.",
"scheduleHelp": "Only the image is updated, and only when its registry publishes a new one.",
"releaseDelayHelp": "A new image is installed only once it is at least this old.",
"ackExternal": "Host directories are not reverted by the backup",
"ackExternalHelp": "This container uses host directories. A scheduled update runs only with this confirmed."
},
"options": {
"title": "Options",
"snapshotBefore": "Snapshot before applying",
@@ -1393,7 +1432,7 @@
"otherPackagesPending": "+{count} other packages pending in the container",
"noManagedUpdateInfo": "No update information yet — check from Security → Secure Gateway.",
"ociTitle": "OCI image container",
"ociBody": "This container was created from an OCI (Docker) image. Update management for OCI containers is coming with the upcoming OCI install feature — updates will rebuild the container from a newer image tag rather than patching packages inside.",
"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.",
"dockerImagesTitle": "Docker images",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
@@ -1652,7 +1691,15 @@
"notifyUpstreamLabel": "Notify me when a new upstream version is available",
"notifyUpstreamHelp": "Sends `app_update_available` to the channels enabled in Settings → Notifications. Turn off if this app can't be updated on your box.",
"excludeFromBadgeLabel": "Exclude from the LXC updates counter",
"excludeFromBadgeHelp": "Don't count this app in the aggregate updates badge on the LXC list card. Useful when you're pinned to a specific version on purpose (tracker requirement, compatibility freeze). Doesn't affect the App tab's own state or the outbound notification."
"excludeFromBadgeHelp": "Don't count this app in the aggregate updates badge on the LXC list card. Useful when you're pinned to a specific version on purpose (tracker requirement, compatibility freeze). Doesn't affect the App tab's own state or the outbound notification.",
"methodOciImage": "OCI image (installed by ProxMenux)",
"ociImageHelp": "The version and its updates are read from the image this container was created from. ProxMenux installed it, so there is nothing to configure: the installed image is compared with the one its registry publishes today.",
"ociAppVersion": "Application",
"ociImage": "Image",
"ociNewImage": "New image",
"ociImageCurrent": "Version",
"refreshData": "Refresh data",
"refreshingData": "Refreshing…"
}
},
"settings": {
@@ -3320,7 +3367,14 @@
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n).",
"ociAppTab": "OCI containers in the App tab — a container installed by OCI manager Apps is recognised from its installation record: the application and its image, a new image detected by digest, and a link to the image repository.",
"ociUpdatesTab": "Updates for OCI containers — Update and Recreate open the same flow as the OCI menu, the backup taken before updating can be kept in a backup storage, and the image can be updated on a schedule.",
"ociLogsTab": "Logs tab for OCI containers — the console output of the application, kept on the host and followed live, with a filter and a download. The Proxmox console of these containers opens a shell.",
"appsUpdateShortcut": "The update icon on the Apps page opens the container straight on its Updates tab.",
"webhookHttps": "Proxmox notifications reach the Monitor with HTTPS enabled — they are delivered on a local-only address and no longer fail with a certificate error.",
"persistentLogs": "A burst of log errors that ended is no longer reported as persistent: a pattern has to keep appearing for 15 minutes, and its warning clears on its own (reported by @Joshua1264).",
"mountsLanAddress": "Mount points on LVM-thin and other block storage show their usage, and multi-container applications open at their LAN address."
}
},
"network": {
@@ -5034,6 +5088,8 @@
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Edit custom link {name}",
"openUpdatesAria": "Open the Updates tab of {name}",
"openUpdatesTitle": "Update available — open the Updates tab",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
+75 -19
View File
@@ -377,7 +377,7 @@
"criticalStatus": "Crítico: requiere atención inmediata",
"warningStatus": "Advertencia: debe ser monitoreado",
"moreIssues": "...y {count} problemas más (ver detalles a continuación)",
"dismissed": "Despedido",
"dismissed": "Descartado",
"occurrences": "Ocurrencias",
"errorSignature": "Firma de error",
"rawMessage": "Mensaje sin procesar",
@@ -1066,10 +1066,11 @@
"tabs": {
"status": "Estado",
"mounts": "Montajes",
"backups": "Copias",
"backups": "Backups",
"updates": "Actualiz.",
"firewall": "Cortafuegos",
"app": "App"
"app": "App",
"logs": "Logs"
},
"actions": {
"start": "Iniciar",
@@ -1087,9 +1088,9 @@
"guest": "Invitado:"
},
"guestTypes": {
"container": "recipiente",
"container": "contenedor",
"vm": "VM",
"containerUi": "Recipiente"
"containerUi": "Contenedor"
},
"backups": {
"title": "Copias de seguridad",
@@ -1146,7 +1147,7 @@
"variables": "Variables: {{cluster}}, {{guestname}}, {{node}}, {{vmid}}",
"creating": "Creando...",
"submit": "Backup",
"typeContainer": "recipiente",
"typeContainer": "contenedor",
"typeVirtualMachine": "máquina virtual",
"modes": {
"snapshot": "Instantánea",
@@ -1169,16 +1170,54 @@
"empty": "Aún no se han registrado eventos del cortafuegos.",
"emptyHint": "Las reglas con log: info o superior completarán esta vista a medida que lleguen los paquetes."
},
"consoleLog": {
"title": "Salida de consola",
"description": "Salida estándar y de errores del proceso principal de la imagen.",
"stopped": "El contenedor está detenido; el log conserva la salida de sus ejecuciones anteriores.",
"lastLines": "Últimas {count} líneas",
"follow": "Seguir",
"pause": "Pausar",
"refresh": "Actualizar",
"download": "Descargar",
"filter": "Filtrar líneas",
"loading": "Cargando la salida de consola...",
"empty": "Todavía no hay salida registrada.",
"emptyHint": "Las líneas aparecen aquí a medida que el contenedor escribe en su consola.",
"noMatches": "Ninguna línea coincide con el filtro.",
"readFailed": "No se pudo leer el log de consola",
"notAvailableTitle": "Este contenedor no tiene log de consola",
"notAvailableHint": "El log de consola se configura cuando ProxMenux instala el contenedor desde su imagen OCI."
},
"ociUpdates": {
"notTracked": "Al registrar la aplicación en la pestaña App, aquí se sigue su imagen.",
"installedImage": "Imagen instalada",
"newImage": "Nueva imagen {image} disponible",
"stackNote": "Forma parte de una aplicación multicontenedor (CT principal {primary}, {count} contenedores): la actualización los abarca a todos.",
"pendingNote": "Una operación sobre este contenedor quedó interrumpida; Recuperar abre su recuperación.",
"update": "Actualizar",
"recreate": "Recrear",
"recover": "Recuperar",
"terminalTitleUpdate": "Actualizar aplicación OCI",
"terminalTitleRecreate": "Recrear aplicación OCI",
"terminalDescription": "El mismo recorrido que OCI manager Apps → Gestionar aplicaciones OCI instaladas.",
"keepBackup": "Conservar el backup previo a la actualización",
"noKeepBackup": "El backup previo a la actualización no se conserva",
"keepBackupHelp": "Cada actualización hace un backup del contenedor antes de sustituir su imagen, para restaurarlo si la actualización falla. Con esta opción ese backup se conserva en el almacenamiento elegido; en Proxmox Backup Server se escribe un backup antes de actualizar.",
"scheduleHelp": "Solo se actualiza la imagen, y solo cuando su registro publica una nueva.",
"releaseDelayHelp": "Una imagen nueva solo se instala cuando tiene al menos esta antigüedad.",
"ackExternal": "Los directorios del host no se revierten con el backup",
"ackExternalHelp": "Este contenedor usa directorios del host. Una actualización programada solo se ejecuta con esto confirmado."
},
"options": {
"title": "Opciones",
"snapshotBefore": "Instantánea antes de aplicar",
"snapshotOn": "— el {storage}",
"noSnapshot": "Sin instantánea previa a la solicitud",
"restartAfter": "Reiniciar el contenedor después de aplicar.",
"noRestart": "Sin reinicio posterior a la aplicación",
"snapshotBefore": "Backup antes de aplicar",
"snapshotOn": "— en {storage}",
"noSnapshot": "Sin backup antes de aplicar",
"restartAfter": "Reiniciar el contenedor después de aplicar",
"noRestart": "Sin reinicio después de aplicar",
"snapshotLabel": "Backup del contenedor antes de aplicar (vzdump)",
"appliesToBoth": "Aplica para ejecuciones manuales + programadas.",
"backupStorage": "Almacenamiento de respaldo",
"backupStorage": "Almacenamiento de backups",
"pickStorage": "Elige un almacenamiento",
"editButton": "Editar",
"cancelButton": "Cancelar",
@@ -1372,7 +1411,7 @@
"otherPackagesPending": "+{count} otros paquetes pendientes en el contenedor",
"noManagedUpdateInfo": "Aún no hay información de actualización: verifique desde Seguridad → Secure Gateway.",
"ociTitle": "Contenedor de imágenes OCI",
"ociBody": "Este contenedor se creó a partir de una imagen OCI (Docker). La gestión de actualizaciones para contenedores OCI viene con la próxima función de instalación de OCI: las actualizaciones reconstruirán el contenedor a partir de una etiqueta de imagen más nueva en lugar de aplicar parches a los paquetes internos.",
"ociBody": "Este contenedor se creó desde una imagen OCI fuera de OCI manager Apps. Se actualiza sustituyendo su imagen, algo que ProxMenux no gestiona en este caso.",
"dockerImagesTitle": "Imágenes Docker",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Imágenes",
@@ -1487,7 +1526,6 @@
"containerNameLabel": "Nombre del contenedor",
"containerNamePlaceholder": "por ejemplo, <nombre-de-su-contenedor>",
"ociLabelKeyLabel": "Clave de etiqueta OCI",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Ruta del intérprete de Python",
"pythonInterpreterPlaceholder": "por ejemplo, /opt/<tu-aplicación>/venv/bin/python",
"pipDistLabel": "Nombre de distribución (paquete pip)",
@@ -1631,7 +1669,16 @@
"notifyUpstreamLabel": "Notificarme cuando haya una nueva versión disponible",
"notifyUpstreamHelp": "Envía `app_update_available` a los canales activos en Ajustes → Notificaciones. Desactívalo si esta app no se puede actualizar en tu instalación.",
"excludeFromBadgeLabel": "Excluir del contador de actualizaciones del LXC",
"excludeFromBadgeHelp": "No sumar esta app al contador agregado de actualizaciones del card del LXC. Útil cuando mantienes una versión concreta a propósito (requisito de tracker, compatibilidad). No afecta al estado que se muestra en la pestaña App ni al envío de la notificación."
"excludeFromBadgeHelp": "No sumar esta app al contador agregado de actualizaciones del card del LXC. Útil cuando mantienes una versión concreta a propósito (requisito de tracker, compatibilidad). No afecta al estado que se muestra en la pestaña App ni al envío de la notificación.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"methodOciImage": "Imagen OCI (instalada por ProxMenux)",
"ociImageHelp": "La versión y sus actualizaciones se leen de la imagen con la que se creó este contenedor. La instaló ProxMenux, así que no hay nada que configurar: la imagen instalada se compara con la que publica hoy su registro.",
"ociAppVersion": "Aplicación",
"ociImage": "Imagen",
"ociNewImage": "Nueva imagen",
"ociImageCurrent": "Versión",
"refreshData": "Actualizar datos",
"refreshingData": "Actualizando…"
},
"statusFilter": {
"ariaLabel": "Filtrar máquinas virtuales y contenedores",
@@ -1702,7 +1749,7 @@
"labels": {
"category": "categoría",
"severity": "gravedad",
"dismissed": "despedido"
"dismissed": "descartado"
},
"errorNames": {
"security_login_attempts": "Intentos de inicio de sesión de seguridad"
@@ -3321,7 +3368,14 @@
"auditReports": "Informes — una auditoría completa más informes enfocados de Revisión de seguridad, Garantía de backups y Capacidad y desgaste, cada uno con su propia cabecera de postura, y un Inventario imprimible; todos se exportan a PDF. La evaluación de seguridad pregunta antes de ejecutar Lynis para que sea rápida.",
"auditPolicyBaseline": "Política y referencia — declara qué se espera del host (backups requeridos, firewall, acceso SSH de root) para que los hallazgos se evalúen contra ello, marca una ejecución como referencia y ve qué cambió desde entonces, con los hallazgos nuevos, resueltos y aceptados por separado.",
"groupedAppUpdates": "Notificaciones de actualización de aplicaciones agrupadas — un mensaje completo por escaneo en lugar de uno por aplicación, agrupado por LXC con las versiones instalada y disponible.",
"adminTokenScope": "Ámbito administrativo — abrir un terminal del Monitor y desactivar la autenticación ahora requieren un token de administrador completo, así que un token de API de solo lectura se mantiene de solo lectura (reportado por @f3rs3n)."
"adminTokenScope": "Ámbito administrativo — abrir un terminal del Monitor y desactivar la autenticación ahora requieren un token de administrador completo, así que un token de API de solo lectura se mantiene de solo lectura (reportado por @f3rs3n).",
"ociAppTab": "Contenedores OCI en la pestaña App — un contenedor instalado por OCI manager Apps se reconoce desde su registro de instalación: la aplicación y su imagen, una imagen nueva detectada por digest y el enlace al repositorio de la imagen.",
"ociUpdatesTab": "Actualizaciones de contenedores OCI — Actualizar y Recrear abren el mismo recorrido que el menú OCI, el backup previo a la actualización puede conservarse en un almacenamiento de backups y la imagen puede actualizarse de forma programada.",
"ociLogsTab": "Pestaña Logs para contenedores OCI — la salida de consola de la aplicación, guardada en el host y seguida en directo, con filtro y descarga. La consola de Proxmox de estos contenedores abre un shell.",
"appsUpdateShortcut": "El icono de actualización de la página Apps abre el contenedor directamente en su pestaña Actualizaciones.",
"webhookHttps": "Las notificaciones de Proxmox llegan al Monitor con HTTPS activado — se entregan en una dirección solo local y ya no fallan por un error de certificado.",
"persistentLogs": "Una ráfaga de errores en los logs que ya terminó no se presenta como persistente: el patrón tiene que seguir apareciendo durante 15 minutos, y su aviso se cierra solo (reportado por @Joshua1264).",
"mountsLanAddress": "Los puntos de montaje en LVM-thin y otros almacenamientos de bloques muestran su uso, y las aplicaciones multicontenedor se abren en su dirección de la LAN."
}
},
"network": {
@@ -4668,7 +4722,7 @@
"updateNow": "Actualizar ahora",
"dismissedItems": "Artículos descartados ({count})",
"permanent": "Permanente",
"dismissed": "Despedido",
"dismissed": "Descartado",
"wasStatus": "era {status}",
"permanentlySuppressed": "Permanentemente suprimido",
"suppressedForMore": "Suprimido por {duration} más",
@@ -4677,7 +4731,7 @@
"lastUpdated": "Última actualización: {date}",
"updateTerminalTitle": "Actualización del sistema Proxmox",
"updateTerminalDescription": "Ejecuta apt-get update + dist-upgrade y limpieza posterior a la actualización en el host.",
"dismiss": "Despedir",
"dismiss": "Descartar",
"silenceFor": "Silencia esta alerta por",
"permanently": "Permanentemente",
"duration": {
@@ -5035,6 +5089,8 @@
"customLinkNewTitle": "Nuevo enlace web",
"customLinkEditTitle": "Editar enlace web",
"customLinkEditAria": "Editar el enlace personalizado {name}",
"openUpdatesAria": "Abrir la pestaña Actualizaciones de {name}",
"openUpdatesTitle": "Actualización disponible — abrir la pestaña Actualizaciones",
"customLinkName": "Nombre",
"customLinkNamePlaceholder": "p. ej. Mi app",
"customLinkUrl": "URL",
+145 -136
View File
@@ -1487,7 +1487,6 @@
"containerNameLabel": "Nom du conteneur",
"containerNamePlaceholder": "par exemple, <votre-nom-de-conteneur>",
"ociLabelKeyLabel": "Clé d'étiquette OCI",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Chemin de l'interpréteur Python",
"pythonInterpreterPlaceholder": "par exemple, /opt/<votre-application>/venv/bin/python",
"pipDistLabel": "Nom de la distribution (paquet pip)",
@@ -1534,11 +1533,8 @@
"portHttps": "https",
"portLogoLabel": "URL du logo pour ce lien (facultatif)",
"portLogoPlaceholder": "par exemple, https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
"portCategoryNone": "No category",
"portCategoryAddNew": "+ Add new category…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
"removePortTooltip": "Supprimer le port",
"detectMethodDpkg": "dpkg ·",
"detectMethodApk": "apk ·",
@@ -1631,7 +1627,11 @@
"notifyUpstreamLabel": "Me prévenir lorsqu'une nouvelle version en amont est disponible",
"notifyUpstreamHelp": "envoie `app_update_available` aux canaux activés dans Paramètres → Notifications. Désactivez-la si cette application ne peut pas être mise à jour sur votre box.",
"excludeFromBadgeLabel": "exclure du compteur de mises à jour LXC",
"excludeFromBadgeHelp": "Ne comptez pas cette application dans le badge de mises à jour globales sur la carte de liste LXC.Utile lorsque vous êtes volontairement épinglé à une version spécifique (exigence de suivi, gel de la compatibilité).N'affecte pas l'état de l'onglet Application ni la notification sortante."
"excludeFromBadgeHelp": "Ne comptez pas cette application dans le badge de mises à jour globales sur la carte de liste LXC.Utile lorsque vous êtes volontairement épinglé à une version spécifique (exigence de suivi, gel de la compatibilité).N'affecte pas l'état de l'onglet Application ni la notification sortante.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"portCategoryPlaceholder": "Catégorie pour le tableau de bord Apps (facultatif)",
"portCategoryCustomPlaceholder": "Tapez une catégorie et appuyez sur Entrée (Esc pour annuler)",
"portCustomUrlPlaceholder": "URL personnalisée (par exemple https://vault.example.com) — remplace IP:port"
},
"statusFilter": {
"ariaLabel": "Filtrer les machines virtuelles et les conteneurs",
@@ -2515,10 +2515,10 @@
"scope": {
"label": "Token permissions",
"readOnly": "Read-only",
"readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.",
"fullAdmin": "Full admin",
"fullAdminHint": "Full control, like your own session.",
"fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations."
"readOnlyHint": "Lire les paramètres et l'état. Recommandé pour les tableaux de bord et les intégrations.",
"fullAdminHint": "Contrôle complet, comme votre propre session.",
"fullAdminWarning": "Un jeton entièrement administratif peut faire tout ce que vous pouvez : désactiver ou redémarrer l'hôte, lancer des mises à jour et ouvrir un terminal. Partagez-le seulement avec des intégrations entièrement fiables."
}
},
"firewall": {
@@ -3302,26 +3302,33 @@
"gotIt": "J'ai compris!",
"dontShowAgain": "Ne plus afficher pour cette version",
"currentFeatures": {
"appDetection": "Smarter app detection in the App tab: Docker is correctly promoted as the parent workload during cold start (Portainer/SearXNG no longer briefly show up as native apps), unregistered suggestions live in the startup cache, and 'Find applications' runs a fresh catalog-backed scan on demand.",
"dockerUpdates": "The Updates tab now covers Docker end-to-end: Docker Engine and per-image update tracking follow the same 24-hour rolling cycle as OS packages, with a 'Check now' action for on-demand digest comparison — no more waiting for the daily collector.",
"appCatalog": "New application detection catalog with over 380 tracked workloads, generated live from community-scripts across seven detector methods (file, binary, dpkg, apk, Python, Docker exec, Docker label). Primary and fallback detectors cover both new and historical LXC layouts.",
"pushover": "Pushover joins Telegram, Gotify, Discord, Email and Apprise as a native notification channel — user/API key, device and sound selectors, priority 0 for regular messages, optional priority 1 for CRITICAL events. Suggested by @benginx (#308).",
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
"auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
"changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"appDetection": "La détection d'applications plus intelligentes dans l'onglet App : Docker est correctement promue en tant que charge de travail parent lors du démarrage à froid (Portainer/SearXNG ne s'affiche plus brièvement en tant qu'applications natives), suggestions non enregistrées en direct dans le cache de démarrage, et 'Trouver des applications' lance un nouveau scanner soutenu par catalogue à la demande.",
"dockerUpdates": "L'onglet Mises à jour couvre maintenant le Docker de bout en bout : Docker Le suivi de la mise à jour du moteur et de l'image suit le même cycle de roulement de 24 heures que les paquets OS, avec une action 'Vérifiez maintenant' pour la comparaison de digest à la demande — pas plus attendre le collecteur quotidien.",
"appCatalog": "Nouveau catalogue de détection d'applications avec plus de 380 charges de travail suivies, généré en direct à partir de scripts communautaires sur sept méthodes de détecteurs (fichier, binaire, dpkg, apk, Python, Docker exec, étiquette Docker). Les détecteurs primaires et les détecteurs de recul couvrent les mises en page nouvelles et historiques du LXC.",
"pushover": "Pushover rejoint Telegram, Gotify, Discord, Email et Apprise en tant que canal de notification natif — touche utilisateur/API, sélecteurs de périphérique et de son, priorité 0 pour les messages réguliers, priorité 1 optionnelle pour les événements CRITIQUES. Proposé par @benginx (#308).",
"appsDashboard": "Nouveau tableau de bord des applications de haut niveau — un lanceur unique pour chaque lien Web à travers le nœud. Applications enregistrées par LXC et Web personnalisé Les liens partagent la même grille avec les badges de catégorie, les liens de recherche et les liens profonds à un clic vers le modal invité.",
"lxcAppsUpdates": "Onglet App à l'intérieur de chaque LXC modal enregistre les applications installées, capture les liens web et suit les versions en amont. L'onglet Mises à jour retravaillées applique les paquets OS et les mises à jour d'applications à partir d'un seul bouton; Docker Moteur et suivi par image suivent le même cycle de 24 heures, avec une action 'Vérifier maintenant' sur demande.",
"multilingual": "Le moniteur parle maintenant 8 langues: anglais, espagnol, allemand, français, italien, portugais, suédois et slovaque. Un énorme merci à @vaso73 pour la construction de l'échafaudage i18n qui a rendu cela possible.",
"nvidiaMultiGpu": "Le cycle de vie du pilote NVIDIA passe à la propriété par BDF de sorte qu'un hôte multi-GPU peut transmettre une carte à un VM et garder l'autre opérationnel sur l'hôte ou en LXCs, plus un noyau + branche + GPU-aware sélecteur de version (#298).",
"aiCustomEndpoint": "AI Assistant personnalisé OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute et tout proxy auto-organisé sur les IP privées, les réseaux loopback ou Docker sont reconnus lors du chargement du catalogue modèle. Le menu déroulant couvre l'erreur du serveur (ou la raison réseau sous-jacente) directement sous le bouton Charger (#325, signalé par @jorgeffonte).",
"secureGatewayArch": "Secure Gateway assistant — le téléchargement de modèle Alpine, la sélection de modèle local et pct create correspondent tous à l'architecture réelle de l'hôte, donc x86 64 hôtes reçoivent amd64 conteneurs et arm64 hôtes reçoivent arm64 conteneurs (#324, rapporté par @N0X4DD0).",
"atomicNotifications": "Les événements de notification réservent leur empreinte digitale de duplication atomiquement avant le traitement de l'IA et la livraison du canal, de sorte que les collecteurs concurrents, les callbacks d'achèvement ou les processus Moniteur parallèles ne peuvent pas envoyer le même événement deux fois. La réservation est libérée lorsqu'aucun canal ne réussit, en préservant les relevés.",
"borgSshPort": "Cible distante Borg — la boîte de dialogue Add Borg destination et le shell TUI acceptent un port SSH personnalisé. BORG RSH, la clé automatique installe le flux et la sonde de capacité tout l'honore. Entièrement en arrière compatible avec les entrées existantes créées sans port explicite (suggéré par @songochain dans la discussion #236).",
"githubToken": "Paramètres → L'API GitHub accepte un jeton d'accès personnel optionnel pour la libération et vérifie les étiquettes lorsque le quota anonyme est épuisé. Le jeton est chiffré au repos et n'est jamais retourné dans le navigateur; l'erreur de limite de taux est traduite dans chaque langue Monitor (suggéré par @SystemIdleProcess dans la discussion #306).",
"replicationContext": "Les notifications de défaillance de réplication natives Proxmox résolvent l'ID de la tâche de réplication, l'ID touché VM/LXC et le nom de l'invité; le bloc d'erreur exact de Proxmox est conservé comme raison, et chaque tâche de réplication se dédouble indépendamment (rapporté par Ale R.).",
"auditAssessment": "Audit & Report — une nouvelle page qui documente et évalue le nœud. L'évaluation comporte un catalogue de vérifications de sécurité, de sauvegardes, de capacité, de stockage, de réseau, de matériel, d'invités et de système, classant chaque constatation comme critique, d'avertissement, d'observation ou conforme et indiquant ce qu'elle lit plutôt que de le juger.",
"changeJournal": "Change journal — a Changes view that listes exact what ProxMenux modified on this host: every configuration file, package and service it touched, with the previous state of chaque, dupliquée to a current-state view that shows the host as it stands now plutôt qu'un journal de chaque exécution.",
"auditReports": "Rapports — un audit complet plus un examen de sécurité ciblé, l'assurance de sauvegarde et rapports de capacité et d'usure, chaque ouverture sur son propre en-tête de posture, et un inventaire imprimable; tous exportent vers PDF. Une évaluation de sécurité demande avant de courir Lynis donc une course reste rapide.",
"auditPolicyBaseline": "Politique et base de référence — déclarer ce que l'hôte est censé être (exigences de sauvegarde, pare-feu, connexion à la racine SSH) de sorte que les constatations notent contre elle, marquent une course comme référence, et voir ce qui a changé puisque, avec les nouvelles, les conclusions résolues et acceptées restent séparées.",
"groupedAppUpdates": "Notifications groupées de mise à jour des applications — un message complet par balayage au lieu d'un message par application, groupé par LXC avec les versions installées et disponibles.",
"adminTokenScope": "Champ d'application administratif — l'ouverture d'un terminal de monitoring et la désactivation de l'authentification nécessitent maintenant un jeton d'administration complet, de sorte qu'un jeton d'API en lecture seule émis à un monitoring intégré reste en lecture seule (rapporté par @f3rs3n).",
"ociAppTab": "Conteneurs OCI dans l'onglet App — un conteneur installé par OCI manager Apps est reconnu à partir de son enregistrement d'installation : l'application et son image, une nouvelle image détectée par digest et un lien vers le dépôt de l'image.",
"ociUpdatesTab": "Mises à jour des conteneurs OCI — Mettre à jour et Recréer ouvrent le même parcours que le menu OCI, la sauvegarde prise avant la mise à jour peut être conservée dans un stockage de sauvegardes et l'image peut être mise à jour de façon planifiée.",
"ociLogsTab": "Onglet Logs pour les conteneurs OCI — la sortie console de l'application, conservée sur l'hôte et suivie en direct, avec un filtre et un téléchargement. La console Proxmox de ces conteneurs ouvre un shell.",
"appsUpdateShortcut": "L'icône de mise à jour de la page Apps ouvre directement le conteneur sur son onglet Mises à jour.",
"webhookHttps": "Les notifications de Proxmox atteignent le Monitor avec HTTPS activé — elles sont livrées sur une adresse uniquement locale et n'échouent plus sur une erreur de certificat.",
"persistentLogs": "Une rafale d'erreurs de logs terminée n'est plus signalée comme persistante : un motif doit continuer d'apparaître pendant 15 minutes, et son avertissement se ferme de lui-même (signalé par @Joshua1264).",
"mountsLanAddress": "Les points de montage sur LVM-thin et autres stockages en mode bloc affichent leur utilisation, et les applications multi-conteneurs s'ouvrent sur leur adresse LAN."
}
},
"network": {
@@ -5027,23 +5034,20 @@
"uncategorized": "Uncategorized",
"openAriaLabel": "Open {name} in a new tab",
"openGuestAriaLabel": "Ouvrir {name} ({type} {id})",
"emptyTitle": "No apps with a web link yet.",
"emptyHint": "Register a Web Link for any app (App tab of a CT) to see it here.",
"customLinkAdd": "Add link",
"editModeToggle": "Edit",
"editModeDone": "Done",
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Modifier le lien personnalisé {name}",
"openUpdatesAria": "Ouvrir l'onglet Mises à jour de {name}",
"openUpdatesTitle": "Mise à jour disponible — ouvrir l'onglet Mises à jour",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
"customLinkLogo": "Logo URL (optional)",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkCategory": "Category (optional)",
"customLinkBinding": "Bound to",
"customLinkBindingNone": "Not bound to any guest",
"customLinkBindingHelp": "Bind this link to a VM or CT to add its ID + name to the card and jump to the guest with one click.",
"customLinkSave": "Save",
"customLinkCreate": "Create",
"customLinkCancel": "Cancel",
@@ -5079,7 +5083,12 @@
"remoteAccessVpn": "Remote Access & VPN",
"webserversProxies": "Webservers & Proxies",
"zigbeeZwaveMatter": "ZigBee, Z-Wave & Matter"
}
},
"emptyTitle": "Pas encore d'applications avec un lien web.",
"emptyHint": "Enregistrez un lien Web pour toute application (onglet App d'un CT) pour le voir ici.",
"customLinkLogoPlaceholder": "Par exemple, https://exemple.com/logo.webp",
"customLinkBindingNone": "Non lié à un invité",
"customLinkBindingHelp": "Relier ce lien à un VM ou CT pour ajouter son ID + nom à la carte et sauter à l'invité en un clic."
},
"audit": {
"presentation": {
@@ -5234,13 +5243,10 @@
"loading": "Loading assessment…",
"run": "Run assessment",
"running": "Assessing…",
"neverRun": "This host has not been assessed yet.",
"lastRun": "Last assessed on {when}",
"stale": "{days} days ago",
"unverifiedChecks": "Non relevés : {checks}. Chacun indique dans ses évidences ce qu'il n'a pas pu lire.",
"noFindings": "No findings match the current filter.",
"affectedCount": "{count} affected",
"acceptedNotice": "{count} accepted risk(s) recorded on this host.",
"reviewOn": "À revoir le : {when}",
"reviewDue": "À revoir — la décision reste en vigueur",
"reviewDueNotice": "{count} décision(s) acceptée(s) sont à revoir.",
@@ -5272,7 +5278,7 @@
"sources": "Sources et dates de collecte"
},
"errors": {
"runFailed": "The assessment could not be started."
"runFailed": "L'évaluation n'a pas pu être entreprise."
},
"checks": {
"backup": {
@@ -5285,7 +5291,7 @@
"uncovered": "{count} invités sur {total} ne sont sélectionnés par aucune tâche de sauvegarde activée",
"excludedData": "{count} exclusions de disques ou montages à vérifier",
"uncoveredExpected": "{required} invités déclarés comme devant être sauvegardés ne sont sélectionnés par aucune tâche activée",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
},
"nextStep": {
"noJobs": "Créez un travail de sauvegarde sur ce noeud et sélectionnez les invités qui détiennent des données que vous ne voudriez pas reconstruire à la main. Un emploi qui existe mais qui est handicapé ne sélectionne rien.",
@@ -5300,10 +5306,10 @@
"rationale": "Ancienneté : temps écoulé depuis la dernière copie stockée. Limite utilisée : ancienneté de référence à laquelle cette copie est comparée.",
"summary": {
"recent": "Les {total} vérifications invité/destination respectent le critère d’ancienneté indiqué",
"stale": "{count} of {total} guests have no backup from the last 30 days",
"noBackups": "No stored backup matches a guest on this node",
"attention": "{count} vérifications invité/destination sur {total} nécessitent un examen",
"evaluationFailed": "The check could not be evaluated"
"stale": "{count} de {total} invités n'ont aucune sauvegarde depuis les 30 derniers jours",
"noBackups": "Aucune sauvegarde enregistrée ne correspond à un invité sur ce nœud",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
},
"nextStep": {
"stale": "Découvrez pourquoi le travail a cessé de produire des copies pour ces invités : il a peut-être été désactivé, son emploi du temps peut ne jamais tirer, ou ses pistes peuvent échouer. La vérification des résultats de l'exécution indique comment chaque travail s'est terminé.",
@@ -5317,10 +5323,10 @@
"rationale": "La rétention telle que Proxmox la résout : réglage de la tâche, puis du stockage, puis la valeur par défaut du nœud. La rétention appliquée par un serveur de sauvegarde n'est pas lisible depuis ce nœud.",
"summary": {
"allDefined": "Les {total} tâches résolvent un réglage de rétention",
"missing": "{count} of {total} enabled job(s) declare no retention",
"notDeclared": "{count} tâches sur {total} conservent toutes les copies : aucune rétention n'est déclarée",
"onServer": "{count} tâches sur {total} écrivent sur un serveur de sauvegarde, qui les élague avec ses propres tâches",
"evaluationFailed": "The check could not be evaluated"
"missing": "{count} de {total} emploi(s) autorisé(s) déclarer aucune rétention",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
},
"nextStep": {
"missing": "Réglez une rétention sur ces tâches, ou sur le stockage auquel ils écrivent. Proxmox prend le réglage du travail d'abord, puis le stockage, puis le noeud par défaut.",
@@ -5383,36 +5389,36 @@
"summary": {
"none": "Rien n'a demandé de redémarrage",
"pending": "{count} éléments sont installés et attendent un redémarrage",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"enterprise_repo_without_subscription": {
"title": "Enterprise repository",
"rationale": "Références à `enterprise.proxmox.com` dans `/etc/apt/sources.list` et `sources.list.d`, face au statut renvoyé par `pvesubscription get`.",
"summary": {
"notEnabled": "The enterprise repository is not enabled",
"subscribed": "The enterprise repository is backed by a subscription",
"unsubscribed": "The enterprise repository is enabled without an active subscription",
"evaluationFailed": "The check could not be evaluated"
"notEnabled": "Le dépôt d'entreprise n'est pas activé",
"subscribed": "Le dépôt d'entreprise est soutenu par un abonnement",
"unsubscribed": "Le dépôt d'entreprise est activé sans abonnement actif",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"memory_overcommit": {
"title": "Memory allocation",
"rationale": "Le plafond `memory` de chaque configuration d'invité face à MemTotal, les invités en cours comptés séparément. Les conteneurs consomment jusqu'à cette limite ; les machines virtuelles sans ballooning la réservent.",
"summary": {
"withinRatio": "Guests are allocated {percent}% of host memory",
"aboveRatio": "Guests are allocated {percent}% of host memory",
"evaluationFailed": "The check could not be evaluated"
"withinRatio": "Les invités reçoivent {percent}% de la mémoire hôte",
"aboveRatio": "Les invités reçoivent {percent}% de la mémoire hôte",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"time_synchronisation": {
"title": "Time synchronisation",
"rationale": "NTP et NTPSynchronized tels que `timedatectl` les rapporte. L'appartenance au cluster, la validation des certificats et l'ordre des journaux dépendent d'horloges concordantes. Un autre mécanisme peut discipliner l'horloge.",
"summary": {
"synchronised": "The clock is synchronised with a time source",
"disabled": "Time synchronisation is disabled",
"notSynchronised": "Time synchronisation is enabled but the clock is not synchronised",
"evaluationFailed": "The check could not be evaluated"
"synchronised": "L'horloge est synchronisée avec une source de temps",
"notSynchronised": "La synchronisation du temps est activée mais l'horloge n'est pas synchronisée",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"kernel_current": {
@@ -5420,21 +5426,21 @@
"rationale": "Le noyau en cours face à celui que l'hôte démarrerait ensuite, tel que `proxmox-boot-tool` le rapporte. Un noyau plus récent seulement installé peut être retenu délibérément ; un écart après un redémarrage indique un démarrage qui n'a pas pris.",
"summary": {
"current": "Le noyau en cours {version} est celui que l'hôte démarrerait ensuite",
"newerAvailable": "The host runs {running} while {newest} is installed",
"newerSelected": "L'hôte exécute {running} et démarrerait {selected} au prochain redémarrage",
"wouldDowngrade": "L'hôte exécute {running} mais démarrerait le plus ancien {selected} au prochain redémarrage",
"bootTargetUnknown": "L'hôte exécute {version} ; le noyau choisi pour le prochain démarrage n'a pas pu être lu",
"evaluationFailed": "The check could not be evaluated"
"newerAvailable": "L'hôte exécute {running} alors que {newest} est installé",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"security_updates": {
"title": "Security updates",
"rationale": "Paquets en attente dont l'origine est un dépôt de sécurité, à partir d'un `apt-get upgrade` simulé. Le nombre reflète ce que rapporte apt, non la gravité de ce que chaque paquet corrige.",
"summary": {
"none": "No package updates are pending",
"noSecurity": "{total} update(s) pending, none from a security repository",
"pending": "{count} of {total} pending update(s) come from a security repository",
"evaluationFailed": "The check could not be evaluated"
"none": "Aucune mise à jour du paquet n'est en cours",
"noSecurity": "{total} mise(s) à jour en attente, aucune depuis un dépôt de sécurité",
"pending": "{count} de {total} en attente de mise(s) à jour proviennent d'un dépôt de sécurité",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"journal_size": {
@@ -5442,10 +5448,10 @@
"rationale": "Le journal sur disque face au plafond qui s'y applique : SystemMaxUse lorsqu'il est défini, sinon la valeur par défaut de journald, un dixième du système de fichiers qui l'héberge.",
"summary": {
"bounded": "Le journal occupe {size}, en deçà de son plafond effectif",
"large": "The journal holds {size} on disk",
"nearCap": "Le journal occupe {size} et atteint {percent}% de son plafond effectif",
"capUnknown": "Le journal occupe {size} ; son plafond effectif n'a pas pu être déterminé",
"evaluationFailed": "The check could not be evaluated"
"large": "La revue détient {size} sur disque",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"swap_configured": {
@@ -5454,7 +5460,7 @@
"summary": {
"active": "{size} of swap is active",
"none": "No swap area is active",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"filesystem_capacity": {
@@ -5500,7 +5506,7 @@
"summary": {
"synchronised": "Les {total} partitions d'amorçage portent les mêmes noyaux",
"attention": "{count} partitions d'amorçage sur {total} demandent un examen",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
},
"rationale": "Le système EFI partitionne les rapports proxmox-boot-tool et les noyaux que chacun transporte. L'état proxmox-boot-tool peut monter temporairement les partitions système EFI ;aucun démarrage n'est tenté."
},
@@ -5510,7 +5516,7 @@
"summary": {
"allRunning": "Les {total} services essentiels sont actifs et aucune unité n'est en échec",
"attention": "{count} constat(s) parmi les unités",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"ha_state": {
@@ -5519,7 +5525,7 @@
"summary": {
"managed": "Les {total} services gérés sont dans un état stabilisé sur {nodes} nœud(s)",
"attention": "{count} constat(s) sur {total} service(s) géré(s)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
}
},
@@ -5528,27 +5534,27 @@
"title": "Container privileges",
"rationale": "Le réglage `unprivileged` de chaque configuration de conteneur. Son absence signifie que le conteneur partage l'espace de noms utilisateur de l'hôte, ce dont certaines charges ont besoin.",
"summary": {
"allUnprivileged": "All {total} containers are unprivileged",
"privileged": "{count} of {total} containers run privileged",
"evaluationFailed": "The check could not be evaluated"
"allUnprivileged": "Tous les conteneurs {total} ne sont pas privilégiés",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"qemu_without_agent": {
"title": "Guest agent on virtual machines",
"rationale": "Le réglage `agent` dans la configuration de chaque machine virtuelle. Le réglage indique que l'agent est déclaré, non qu'il répond.",
"summary": {
"allHaveAgent": "All {total} virtual machines declare the guest agent",
"missingAgent": "{count} of {total} virtual machines do not declare the guest agent",
"evaluationFailed": "The check could not be evaluated"
}
"allHaveAgent": "Toutes les machines virtuelles {total} déclarent l'agent invité",
"missingAgent": "{count} des machines virtuelles {total} ne déclarent pas l'agent invité",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
},
"title": "Agent invité sur machines virtuelles"
},
"autostart": {
"title": "Automatic start",
"rationale": "Le réglage `onboot` de chaque invité, hors modèles et invités gérés par HA. Qu'un invité doive revenir de lui-même relève de la politique déclarée.",
"summary": {
"allAutostart": "All {total} guests start with the host",
"notAutostart": "{count} of {total} guests do not start with the host",
"evaluationFailed": "The check could not be evaluated"
"allAutostart": "Tous les invités {total} commencent par l'hôte",
"notAutostart": "{count} des invités {total} ne commencent pas avec l'hôte",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"stuck_snapshots": {
@@ -5556,28 +5562,28 @@
"rationale": "État et ancienneté des snapshots ainsi que les tâches actives. Une opération récente ou sans date vérifiable n'est pas considérée comme interrompue.",
"summary": {
"noSnapshots": "No guest holds snapshots",
"allComplete": "The {total} snapshot(s) are complete",
"stuck": "{count} of {total} snapshot(s) were left mid-operation",
"evaluationFailed": "The check could not be evaluated"
"allComplete": "Les instantanés {total} sont complets",
"stuck": "{count} du ou des instantanés {total} ont été laissés en milieu de fonctionnement",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"cpu_host_type": {
"title": "Virtual CPU model",
"rationale": "La valeur `cpu` de chaque machine virtuelle. `host` expose le jeu d'instructions du processeur physique, ce qui restreint les nœuds vers lesquels l'invité peut migrer. L'incompatibilité avec une destination précise n'est pas déterminée ici.",
"summary": {
"none": "None of the {total} virtual machines is pinned to the host processor",
"pinned": "{count} of {total} virtual machines are pinned to the host processor",
"evaluationFailed": "The check could not be evaluated"
"none": "Aucune des machines virtuelles {total} n'est épinglée au processeur hôte",
"pinned": "{count} des machines virtuelles {total} sont épinglées au processeur hôte",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"replication_state": {
"title": "Replication",
"rationale": "Tâches de réplication issues de l'API : nombre d'échecs, dernière erreur, dernière synchronisation et le calendrier que chaque tâche déclare. Les tâches en pause sont signalées comme telles.",
"summary": {
"healthy": "The {total} replication job(s) report no error",
"failing": "{count} of {total} replication job(s) report an error",
"statusUnavailable": "Replication jobs are defined but their status could not be read",
"evaluationFailed": "The check could not be evaluated"
"healthy": "La ou les tâches de réplication {total} ne signalent aucune erreur",
"failing": "{count} des tâches de réplication {total} signalent une erreur",
"statusUnavailable": "Les emplois de replication sont définis mais leur statut n'a pas pu être lu.",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
}
},
@@ -5589,39 +5595,39 @@
"bothEnabled": "L'activation du pare-feu est configurée au niveau du datacenter et du nœud",
"datacenterOff": "Le pare-feu est désactivé au niveau du datacenter ; les règles du nœud ne sont donc pas appliquées",
"nodeOff": "Le pare-feu est activé au niveau du datacenter, mais pas sur ce nœud",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"lynis_warnings": {
"title": "Lynis warnings",
"rationale": "Avertissements du dernier audit Lynis, chacun avec son identifiant de test, et l'ancienneté de cet audit. Un audit n'est lancé que si Lynis est installé et qu'aucun rapport complet n'existe. Les suggestions ne sont pas incluses.",
"summary": {
"none": "The last Lynis audit recorded no warnings",
"found": "The last Lynis audit recorded {count} warning(s)",
"incomplete": "The Lynis report is incomplete",
"evaluationFailed": "The check could not be evaluated",
"noneStale": "Le dernier audit Lynis n'a relevé aucun avertissement, et son rapport a {days} jour(s)",
"foundStale": "Le dernier audit Lynis a relevé {count} avertissement(s), et son rapport a {days} jour(s)"
"foundStale": "Le dernier audit Lynis a relevé {count} avertissement(s), et son rapport a {days} jour(s)",
"none": "La dernière vérification de Lynis n'a enregistré aucun avertissement",
"found": "La dernière vérification de Lynis a enregistré des avertissements {count}",
"incomplete": "Le rapport Lynis est incomplet",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"certificate_expiry": {
"title": "Certificate validity",
"rationale": "La date d'expiration du certificat que pveproxy sert depuis /etc/pve/local. Un certificat personnalisé prime sur celui que Proxmox génère.",
"summary": {
"valid": "The certificate is valid for {days} more day(s)",
"expiring": "The certificate expires in {days} day(s)",
"expired": "The certificate expired {days} day(s) ago",
"evaluationFailed": "The check could not be evaluated"
"valid": "Le certificat est valide pour {days} plus jour(s)",
"expiring": "Le certificat expire en {days} jour(s)",
"expired": "Le certificat a expiré {days} jour(s)",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"ssh_root_login": {
"title": "SSH root access",
"rationale": "PermitRootLogin dans la configuration effective de `sshd -T`, avec les méthodes d'authentification associées. Proxmox est livré avec `yes`, qui accepte un mot de passe.",
"summary": {
"password": "Root may sign in over SSH with a password",
"keyOnly": "Root may sign in over SSH with a key only",
"denied": "Root may not sign in over SSH",
"evaluationFailed": "The check could not be evaluated"
"password": "Root peut se connecter sur SSH avec un mot de passe",
"keyOnly": "La racine peut se connecter sur SSH avec une clé seulement",
"denied": "La racine ne peut pas se connecter au-dessus de SSH",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
}
},
@@ -5630,9 +5636,9 @@
"title": "Volume assignment",
"rationale": "Volumes d'invité sur le stockage local face aux références des configurations actuelles, en attente et de snapshots. Les sauvegardes, ISO et modèles restent hors de la comparaison. Un volume sans référence est un candidat à examiner.",
"summary": {
"none": "No orphaned volumes were found",
"found": "{count} volumes sans référence dans les configurations examinées",
"evaluationFailed": "The check could not be evaluated"
"none": "Aucun volume d'orphelins n'a été trouvé",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"zfs_arc_max": {
@@ -5640,21 +5646,21 @@
"rationale": "Valeurs effectives de c_min, c_max et taille de l'ARC, le paramètre de module chargé et les réglages persistants de /etc/modprobe.d. Une valeur configurée à zéro sélectionne la valeur par défaut du module ; l'ARC est un plafond et la mémoire qu'il occupe est récupérable.",
"summary": {
"bounded": "La limite de l'ARC représente {percent}% de la mémoire de l'hôte, et la mémoire qu'elle occupe est récupérable",
"high": "The ARC may use {percent}% of host memory",
"unset": "The ARC has no explicit limit set",
"conflicting": "{count} réglages ARC ne concordent pas entre eux",
"pending": "Un réglage ARC persistant diffère de la valeur portée par le module en cours",
"evaluationFailed": "The check could not be evaluated"
"high": "L'ARC peut utiliser {percent}% de la mémoire hôte",
"unset": "L'ARC n'a pas de limite explicite",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"zfs_scrub_age": {
"title": "ZFS scrub",
"rationale": "Le dernier scrub terminé que `zpool status` rapporte pour chaque pool. Un resilver n'est pas un scrub. Un pool créé récemment n'a pas encore eu l'occasion d'en effectuer un.",
"summary": {
"recent": "The {total} pool(s) were scrubbed within the last 35 days",
"overdue": "{count} of {total} pool(s) have not been scrubbed in 35 days",
"neverScrubbed": "{count} pool(s) record no scrub",
"evaluationFailed": "The check could not be evaluated"
"recent": "Les piscines {total} ont été nettoyées au cours des 35 derniers jours.",
"overdue": "{count} de la ou des piscines {total} n'ont pas été nettoyées en 35 jours",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"thin_pool_overprovisioning": {
@@ -5664,7 +5670,7 @@
"withinRatio": "Les {total} thin pools restent sous les seuils d'examen appliqués",
"aboveRatio": "{count} pools légers sur {total} distribuent plus de capacité qu'ils n'en possèdent",
"pressure": "{pressure} pools légers sur {total} approchent le remplissage de leurs données ou métadonnées",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"connected_storage": {
@@ -5673,7 +5679,7 @@
"summary": {
"available": "PVE indique que les {total} stockages sont disponibles ; les composants internes distants et l'accès en écriture n'ont pas été testés",
"attention": "{count} stockages sur {total} nécessitent un examen",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"pool_integrity": {
@@ -5691,7 +5697,7 @@
"summary": {
"healthy": "Ceph rapporte HEALTH_OK",
"degraded": "Ceph rapporte {state}, avec {count} vérification(s) nommée(s)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"array_integrity": {
@@ -5700,7 +5706,7 @@
"summary": {
"intact": "Les {total} grappes et cartes conservent leur redondance",
"degraded": "{count} grappes ou cartes sur {total} en manquent",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
}
},
@@ -5712,7 +5718,7 @@
"withinLife": "Les {total} relevés de disques ne dépassent pas le seuil indicatif de cinq ans",
"pastLife": "{count} relevés de disques sur {total} dépassent cinq ans de service",
"noReadings": "Aucun disque ne rapporte de compteurs SMART exploitables ({skipped} sans relevés)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"disk_errors": {
@@ -5729,23 +5735,22 @@
"title": "Bond members",
"rationale": "L'état MII de chaque membre du bond selon /proc/net/bonding et le nombre de liens restants. En active-backup, un membre de secours se déclare actif et ne transporte rien.",
"summary": {
"allUp": "All members of the {total} bond(s) are up",
"membersDown": "{count} bond member(s) are not up",
"evaluationFailed": "The check could not be evaluated"
"allUp": "Tous les membres des obligations {total} sont en hausse",
"membersDown": "Les membres d'obligations {count} ne sont pas en hausse",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
},
"bridge_without_ports": {
"title": "Bridge ports",
"rationale": "La configuration des ports de chaque bridge. Un bridge sans port physique dessert un réseau interne ou routé.",
"summary": {
"allConnected": "The {total} bridge(s) carry a port",
"isolated": "{count} of {total} bridge(s) carry no port",
"evaluationFailed": "The check could not be evaluated"
"allConnected": "Le ou les ponts {total} portent un port",
"evaluationFailed": "La vérification n'a pas pu être évaluée"
}
}
}
},
"summaryFallback": "The check could not be evaluated",
"acceptRisk": {
"action": "Accepter le risque",
"revoke": "Remettre en actif",
@@ -5779,17 +5784,15 @@
"OK": "OK"
},
"viewSwitch": {
"ariaLabel": "Switch between assessment and inventory",
"assessment": "Assessment",
"inventory": "Inventory",
"policy": "Politique",
"changes": "Modifications"
"changes": "Modifications",
"ariaLabel": "Changement entre l'évaluation et l'inventaire"
},
"inventory": {
"loading": "Loading inventory…",
"failed": "The inventory could not be composed.",
"collectedAt": "Composed on {when}",
"unavailable": "Not read in this inventory",
"unresolved": "path not resolved",
"noUplink": "no uplink",
"identity": "Node identity",
@@ -5832,12 +5835,10 @@
"protection": "Backup",
"passthrough": "Passthrough",
"noBackup": "No backup",
"noBackupDetail": "No enabled backup job selects this guest.",
"applications": "Applications",
"versionUnknown": "version not detected",
"passthroughTitle": "PCI passthrough",
"iommuGroup": "IOMMU group {group}",
"sharedGroup": "{count} more device(s) in the same group",
"proxmenux": "ProxMenux optimizations",
"latency": "Latence réseau",
"subscriptionStatus": {
@@ -5848,7 +5849,11 @@
"suspended": "Suspendu",
"new": "En attente d'activation",
"unknown": "Inconnu"
}
},
"failed": "L'inventaire n'a pas pu être composé.",
"unavailable": "Non lu dans cet inventaire",
"noBackupDetail": "Aucune tâche de sauvegarde activée ne sélectionne cet invité.",
"sharedGroup": "{count} plus de dispositif(s) dans le même groupe"
},
"profile": {
"label": "Report",
@@ -5871,7 +5876,6 @@
"area": "Area",
"inventoryAnnex": "Inventory annex",
"scopeTitle": "Scope of this report",
"scopeBody": "This report describes the Proxmox VE node named above, as observed from the node itself at the time stated. It does not cover the interior of the guests beyond what they declare, network equipment outside the host, physical infrastructure, or any dependency not visible from this node. Findings marked as not determined were not measured and are not evidence of absence.",
"building": "Composition du rapport…",
"node": "Nœud",
"profile": "Profil",
@@ -6020,11 +6024,12 @@
"structureSubtitle": "Comment {node} est construit et configuré",
"postureTitle": "Posture",
"posture": {
"security": "Host exposure and access.",
"backup": "Guest protection and whether it is real.",
"capacity": "Room to grow and the wear on the disks."
"security": "Exposition de l'hôte et accès.",
"backup": "Protection des invités et si elle est réelle.",
"capacity": "La place pour grandir et l'usure sur les disques."
},
"scopeReadOnly": "L’évaluation inspecte les paramètres et l’état de l’hôte. Il peut rédiger des rapports et des journaux ;les vérifications de l'état de démarrage peuvent monter temporairement les partitions système EFI."
"scopeReadOnly": "L’évaluation inspecte les paramètres et l’état de l’hôte. Il peut rédiger des rapports et des journaux ;les vérifications de l'état de démarrage peuvent monter temporairement les partitions système EFI.",
"scopeBody": "Le présent rapport décrit le noeud Proxmox VE nommé ci-dessus, comme on l'a vu dans le nœud lui-même à l'époque indiquée. Il ne couvre pas l'intérieur des invités au-delà de ce qu'ils déclarent, l'équipement réseau en dehors de l'hôte, l'infrastructure physique, ou toute dépendance non visible de ce noeud. Les résultats marqués comme non déterminés n'ont pas été mesurés et ne sont pas une preuve d'absence."
},
"results": "Résultats",
"classifications": {
@@ -6178,15 +6183,19 @@
"notApplicableScope": "Rien dans le périmètre examiné auquel cette vérification s'applique.",
"lynis": {
"title": "Run Lynis",
"bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.",
"bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.",
"withLynis": "Run with Lynis",
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
"cancel": "Cancel",
"bodyNotRun": "Lynis est installé mais n'a pas encore été exécuté. Il termine maintenant l'examen de sécurité, mais le processus peut prendre quelques minutes.",
"bodyStale": "Le rapport Lynis date de {days} jours. Vous pouvez l'exécuter maintenant pour actualiser les données (il faut un peu plus longtemps) ou continuer avec le rapport existant."
},
"readOnlyNotice": "L’évaluation inspecte les paramètres et l’état de l’hôte. Il peut rédiger des rapports et des journaux ;les vérifications de l'état de démarrage peuvent monter temporairement les partitions système EFI.",
"noActionNeeded": "Rien à faire. Ce chèque a trouvé ce qu'il s'attend à trouver.",
"couldNotEvaluate": "Cette vérification n'a pu être évaluée, de sorte qu'elle ne fait aucun rapport. La preuve enregistre ce qu'elle n'a pas pu lire."
"couldNotEvaluate": "Cette vérification n'a pu être évaluée, de sorte qu'elle ne fait aucun rapport. La preuve enregistre ce qu'elle n'a pas pu lire.",
"neverRun": "Cet hôte n'a pas encore été évalué.",
"noFindings": "Aucune découverte ne correspond au filtre actuel.",
"acceptedNotice": "{count} a accepté les risques enregistrés sur cet hôte.",
"summaryFallback": "La vérification n'a pas pu être évaluée"
},
"runtime": {
"notifications": {
+145 -136
View File
@@ -1487,7 +1487,6 @@
"containerNameLabel": "Nome del contenitore",
"containerNamePlaceholder": "ad esempio, <nome-contenitore>",
"ociLabelKeyLabel": "Chiave etichetta OCI",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Percorso dell'interprete Python",
"pythonInterpreterPlaceholder": "ad esempio, /opt/<tua-app>/venv/bin/python",
"pipDistLabel": "Nome della distribuzione (pacchetto pip)",
@@ -1534,11 +1533,8 @@
"portHttps": "https",
"portLogoLabel": "URL del logo per questo collegamento (facoltativo)",
"portLogoPlaceholder": "ad esempio, https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
"portCategoryNone": "No category",
"portCategoryAddNew": "+ Add new category…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
"removePortTooltip": "Rimuovere la porta",
"detectMethodDpkg": "dpkg ·",
"detectMethodApk": "apk ·",
@@ -1631,7 +1627,11 @@
"notifyUpstreamLabel": "avvisami quando è disponibile una nuova versione upstream",
"notifyUpstreamHelp": "invia `app_update_available` ai canali abilitati in Impostazioni → Notifiche. Disattiva se questa app non può essere aggiornata sul tuo box.",
"excludeFromBadgeLabel": "esclusione dal contatore degli aggiornamenti LXC",
"excludeFromBadgeHelp": "non contare questa app nel badge degli aggiornamenti aggregati sulla scheda dell'elenco LXC.Utile quando sei bloccato di proposito su una versione specifica (requisito del tracker, blocco della compatibilità).Non influisce sullo stato della scheda App o sulla notifica in uscita."
"excludeFromBadgeHelp": "non contare questa app nel badge degli aggiornamenti aggregati sulla scheda dell'elenco LXC.Utile quando sei bloccato di proposito su una versione specifica (requisito del tracker, blocco della compatibilità).Non influisce sullo stato della scheda App o sulla notifica in uscita.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"portCategoryPlaceholder": "Categoria per il cruscotto App (opzionale)",
"portCategoryCustomPlaceholder": "Digitare una categoria e premere Invio (Esc per annullare)",
"portCustomUrlPlaceholder": "URL personalizzato (ad esempio https://vault.example.com) — overrides IP:port"
},
"statusFilter": {
"ariaLabel": "Filtra macchine virtuali e container",
@@ -2515,10 +2515,10 @@
"scope": {
"label": "Token permissions",
"readOnly": "Read-only",
"readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.",
"fullAdmin": "Full admin",
"fullAdminHint": "Full control, like your own session.",
"fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations."
"readOnlyHint": "Legge metriche e stato. Consigliato per cruscotti e integrazioni.",
"fullAdminHint": "Controllo completo, come la tua sessione.",
"fullAdminWarning": "Un token full-admin può fare tutto il possibile: spegnere o riavviare l'host, eseguire aggiornamenti e aprire un terminale. Condividi solo con integrazioni completamente affidabili."
}
},
"firewall": {
@@ -3302,26 +3302,33 @@
"gotIt": "Fatto!",
"dontShowAgain": "Non mostrare più per questa versione",
"currentFeatures": {
"appDetection": "Smarter app detection in the App tab: Docker is correctly promoted as the parent workload during cold start (Portainer/SearXNG no longer briefly show up as native apps), unregistered suggestions live in the startup cache, and 'Find applications' runs a fresh catalog-backed scan on demand.",
"dockerUpdates": "The Updates tab now covers Docker end-to-end: Docker Engine and per-image update tracking follow the same 24-hour rolling cycle as OS packages, with a 'Check now' action for on-demand digest comparison — no more waiting for the daily collector.",
"appCatalog": "New application detection catalog with over 380 tracked workloads, generated live from community-scripts across seven detector methods (file, binary, dpkg, apk, Python, Docker exec, Docker label). Primary and fallback detectors cover both new and historical LXC layouts.",
"pushover": "Pushover joins Telegram, Gotify, Discord, Email and Apprise as a native notification channel — user/API key, device and sound selectors, priority 0 for regular messages, optional priority 1 for CRITICAL events. Suggested by @benginx (#308).",
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
"auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
"changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"appDetection": "Rilevamento delle app più intelligenti nella scheda App: Docker è correttamente promosso come il carico di lavoro dei genitori durante l'avvio a freddo (Portainer/SearXNG non si presenta più brevemente come applicazioni native), suggerimenti non registrati dal vivo nella cache di avvio, e 'Find application' esegue una nuova scansione a catalogo a richiesta.",
"dockerUpdates": "La scheda Aggiornamenti ora copre Docker end-to-end: Docker Aggiornamento motore e per immagine seguire lo stesso ciclo di rotolamento 24 ore di pacchetti OS, con un'azione 'Check now' per il confronto on-demand digest — non più in attesa per il collettore quotidiano.",
"appCatalog": "Nuovo catalogo di rilevamento delle applicazioni con oltre 380 carichi di lavoro tracciati, generati dal vivo da codici comunitari attraverso sette metodi di rivelatore (file, binario, dpkg, apk, Python, Docker exec, Docker label). I rilevatori primari e fallback coprono sia i nuovi che storici layout LXC.",
"pushover": "Pushover unisce Telegram, Gotify, Discord, Email e Apprise come canale di notifica nativo — chiave utente/API, selettori di dispositivo e suono, priorità 0 per messaggi regolari, priorità opzionale 1 per eventi CRITICAL. Suggerito da @benginx (#308).",
"appsDashboard": "Nuovo cruscotto App di alto livello — un singolo lanciatore per ogni collegamento Web attraverso il nodo. Applicazioni registrate da LXC e Web personalizzato definito dall'utente I collegamenti condividono la stessa griglia con i distintivi di categoria, la ricerca e un clic deep-link torna al modal guest.",
"lxcAppsUpdates": "Scheda App all'interno di ogni LXC modal registra app installate, cattura weblink e traccia versioni a monte. La scheda Aggiornamenti rielaborati applica i pacchetti OS e gli aggiornamenti delle app da un singolo pulsante; Docker Engine e per-image tracking seguono lo stesso ciclo di 24 ore, con un'azione 'Check now' a richiesta.",
"multilingual": "Il Monitor ora parla 8 lingue: inglese, spagnolo, tedesco, francese, italiano, portoghese, svedese e slovacco. Enorme grazie a @vaso73 per la costruzione del ponteggio i18n che ha reso possibile questo.",
"nvidiaMultiGpu": "Il ciclo di vita del driver NVIDIA si sposta alla proprietà per-BDF in modo che un host multi-GPU possa passare una scheda a un VM e mantenere l'altra operativa sull'host o in LXCs, oltre a un kernel + branch + GPU-aware versione picker (#298).",
"aiCustomEndpoint": "AI Assistant Custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute e qualsiasi proxy self-hosted su IP privati, loopback o reti Docker sono riconosciuti al momento del caricamento del catalogo del modello. Il dropdown supera l'errore del server (o la ragione di rete sottostante) direttamente sotto il pulsante Carica (#325, riportato da @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — il download del modello alpino, la selezione dei modelli locali e pct create tutti corrispondono all'architettura reale dell'host, così gli host x86 64 ricevono i container amd64 e gli host arm64 ricevono i container arm64 (#324, segnalati da @N0X4DD0).",
"atomicNotifications": "Gli eventi di notifica riservano le loro impronte di deduplicazione atomicamente prima dell'elaborazione di AI e della consegna dei canali, così i collettori concomitanti, i callback di completamento o i processi di Monitor parallelo non possono inviare lo stesso evento due volte. La prenotazione viene rilasciata quando nessun canale riesce, conservando i retries.",
"borgSshPort": "Borg obiettivo remoto — la finestra di dialogo Aggiungi destinazione Borg e la shell TUI accettano una porta SSH personalizzata. BORG RSH, il flusso di installazione chiave automatica e la sonda di capacità lo onorano tutti. Pienamente compatibile con le voci esistenti create senza un porto esplicito (suggested da @songochain in discussione #236).",
"githubToken": "Impostazioni → GitHub API accetta un gettone di accesso personale facoltativo per il rilascio e i controlli dei tag quando la quota anonima è esaurita. Il token è crittografato a riposo e non è mai tornato al browser; l'errore di tasso-limit è tradotto in ogni lingua Monitor (suggested da @SystemIdleProcess in discussione #306).",
"replicationContext": "Le notifiche di errori di replica Proxmox native risolvono l'ID del lavoro di replica, l'ID VM/LXC interessato e il nome dell'ospite; il blocco di errore esatto da Proxmox è conservato come il motivo, e ogni lavoro di replica deduplica indipendentemente (riportato da Ale R.).",
"auditAssessment": "Audit & Report — una nuova pagina che documenta e valuta il nodo. La valutazione gestisce un catalogo di controlli su sicurezza, backup, capacità, storage, rete, hardware, ospiti e sistema, classificando ogni ricerca come critico, avviso, osservazione o conformante e affermando ciò che legge anziché giudicarlo.",
"changeJournal": "Cambia la rivista — una vista Modifiche che elenca esattamente ciò che ProxMenux modificato su questo host: ogni file di configurazione, pacchetto e servizio che ha toccato, con lo stato precedente di ciascuno, deduplicato a una vista corrente-stato che mostra l'host come si trova ora piuttosto che un registro di ogni corsa.",
"auditReports": "Rapporti — una revisione completa di audit più focalizzata di sicurezza, garanzia di backup e report di capacità & usura, ogni apertura sul proprio intestazione postura, e un Inventory stampabile; tutte le esportazioni in PDF. Una valutazione di sicurezza chiede prima di eseguire Lynis così una corsa rimane veloce.",
"auditPolicyBaseline": "Politica e linea di base — dichiarare ciò che l'host è previsto per essere (requisiti di backup, firewall, root SSH login) in modo da trovare grado contro di esso, contrassegnare una corsa come il riferimento, e vedere che cosa cambiato da, con nuovi, risolti e accettati risultati tenuti separati.",
"groupedAppUpdates": "Notifiche di aggiornamento dell'applicazione raggruppate — un messaggio completo per scansione invece di uno per applicazione, raggruppate da LXC con le versioni installate e disponibili.",
"adminTokenScope": "Amministrativo — l'apertura di un terminale Monitor e la disattivazione dell'autenticazione richiedono ora un token full-admin, quindi un token API di sola lettura rilasciato ad un'integrazione di monitoraggio resta in sola lettura (reported by @f3rs3n).",
"ociAppTab": "Container OCI nella scheda App — un container installato da OCI manager Apps viene riconosciuto dal suo registro di installazione: l'applicazione e la sua immagine, una nuova immagine rilevata tramite digest e un link al repository dell'immagine.",
"ociUpdatesTab": "Aggiornamenti dei container OCI — Aggiorna e Ricrea aprono lo stesso percorso del menu OCI, il backup eseguito prima dell'aggiornamento può essere conservato in uno storage di backup e l'immagine può essere aggiornata in modo pianificato.",
"ociLogsTab": "Scheda Logs per i container OCI — l'output della console dell'applicazione, conservato sull'host e seguito in tempo reale, con filtro e download. La console di Proxmox di questi container apre una shell.",
"appsUpdateShortcut": "L'icona di aggiornamento nella pagina App apre il container direttamente sulla sua scheda Aggiornamenti.",
"webhookHttps": "Le notifiche di Proxmox raggiungono il Monitor con HTTPS attivo — vengono consegnate su un indirizzo solo locale e non falliscono più per un errore di certificato.",
"persistentLogs": "Una raffica di errori nei log che si è conclusa non viene più segnalata come persistente: un pattern deve continuare a comparire per 15 minuti, e il suo avviso si chiude da solo (segnalato da @Joshua1264).",
"mountsLanAddress": "I punti di mount su LVM-thin e altri storage a blocchi mostrano il loro utilizzo, e le applicazioni multi-container si aprono sul loro indirizzo LAN."
}
},
"network": {
@@ -5027,23 +5034,20 @@
"uncategorized": "Uncategorized",
"openAriaLabel": "Open {name} in a new tab",
"openGuestAriaLabel": "Apri {name} ({type} {id})",
"emptyTitle": "No apps with a web link yet.",
"emptyHint": "Register a Web Link for any app (App tab of a CT) to see it here.",
"customLinkAdd": "Add link",
"editModeToggle": "Edit",
"editModeDone": "Done",
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Modifica il collegamento personalizzato {name}",
"openUpdatesAria": "Apri la scheda Aggiornamenti di {name}",
"openUpdatesTitle": "Aggiornamento disponibile — apri la scheda Aggiornamenti",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
"customLinkLogo": "Logo URL (optional)",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkCategory": "Category (optional)",
"customLinkBinding": "Bound to",
"customLinkBindingNone": "Not bound to any guest",
"customLinkBindingHelp": "Bind this link to a VM or CT to add its ID + name to the card and jump to the guest with one click.",
"customLinkSave": "Save",
"customLinkCreate": "Create",
"customLinkCancel": "Cancel",
@@ -5079,7 +5083,12 @@
"remoteAccessVpn": "Remote Access & VPN",
"webserversProxies": "Webservers & Proxies",
"zigbeeZwaveMatter": "ZigBee, Z-Wave & Matter"
}
},
"emptyTitle": "Nessuna applicazione con un link web ancora.",
"emptyHint": "Registrare un Web Link per qualsiasi applicazione (App scheda di un CT) per vederlo qui.",
"customLinkLogoPlaceholder": "ad esempio, https://example.com/logo.webp",
"customLinkBindingNone": "Non vincolato a nessun ospite",
"customLinkBindingHelp": "Collegare questo link a un VM o CT per aggiungere il suo ID + nome alla scheda e saltare all'ospite con un clic."
},
"audit": {
"presentation": {
@@ -5234,13 +5243,10 @@
"loading": "Loading assessment…",
"run": "Run assessment",
"running": "Assessing…",
"neverRun": "This host has not been assessed yet.",
"lastRun": "Last assessed on {when}",
"stale": "{days} days ago",
"unverifiedChecks": "Non effettuate: {checks}. Ciascuna dice nella propria evidenza cosa non ha potuto leggere.",
"noFindings": "No findings match the current filter.",
"affectedCount": "{count} affected",
"acceptedNotice": "{count} accepted risk(s) recorded on this host.",
"reviewOn": "Da rivedere il: {when}",
"reviewDue": "Da rivedere — la decisione resta valida",
"reviewDueNotice": "{count} decisione/i accettata/e da rivedere.",
@@ -5272,7 +5278,7 @@
"sources": "Fonti e date di raccolta"
},
"errors": {
"runFailed": "The assessment could not be started."
"runFailed": "La valutazione non potrebbe essere iniziata."
},
"checks": {
"backup": {
@@ -5285,7 +5291,7 @@
"uncovered": "{count} guest su {total} non sono selezionati da alcun processo di backup attivo",
"excludedData": "{count} esclusioni di dischi o mount point da verificare",
"uncoveredExpected": "{required} guest dichiarati come da proteggere non sono selezionati da alcun processo attivo",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
},
"nextStep": {
"noJobs": "Creare un lavoro di backup su questo nodo e selezionare gli ospiti in possesso di dati che non si desidera ricostruire a mano. Un lavoro che esiste ma è disabilitato non seleziona nulla.",
@@ -5300,10 +5306,10 @@
"rationale": "Età: tempo trascorso dall’ultima copia archiviata. Limite utilizzato: età di riferimento con cui viene confrontata la copia.",
"summary": {
"recent": "Tutte le {total} verifiche guest/destinazione rispettano il criterio di anzianità indicato",
"stale": "{count} of {total} guests have no backup from the last 30 days",
"noBackups": "No stored backup matches a guest on this node",
"attention": "{count} verifiche guest/destinazione su {total} richiedono attenzione",
"evaluationFailed": "The check could not be evaluated"
"stale": "{count} degli ospiti di {total} non hanno alcun backup dagli ultimi 30 giorni",
"noBackups": "Nessun backup memorizzato corrisponde a un ospite su questo nodo",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
},
"nextStep": {
"stale": "Scopri perché il lavoro ha smesso di produrre copie per questi ospiti: potrebbe essere stato disabilitato, il suo programma potrebbe non sparare, o le sue piste potrebbero essere in fallimento. I risultati dell'esecuzione controllano i rapporti come ogni lavoro è finito.",
@@ -5317,10 +5323,10 @@
"rationale": "La ritenzione come la risolve Proxmox: impostazione del processo, poi dello storage, poi il valore predefinito del nodo. La ritenzione applicata da un server di backup non è leggibile da questo nodo.",
"summary": {
"allDefined": "Tutti i {total} processi risolvono un'impostazione di ritenzione",
"missing": "{count} of {total} enabled job(s) declare no retention",
"notDeclared": "{count} processi su {total} conservano ogni copia: non hanno una ritenzione dichiarata",
"onServer": "{count} processi su {total} scrivono su un server di backup, che le pota con i propri processi",
"evaluationFailed": "The check could not be evaluated"
"missing": "{count} di {total} abilitato lavori(i) non dichiarano nessuna ritenzione",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
},
"nextStep": {
"missing": "Impostare una ritenzione su questi lavori, o sullo storage a cui scrivono. Proxmox prende l'impostazione del lavoro prima, poi lo storage è, poi il nodo predefinito.",
@@ -5383,36 +5389,36 @@
"summary": {
"none": "Nulla ha richiesto un riavvio",
"pending": "{count} elementi sono installati e attendono un riavvio",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"enterprise_repo_without_subscription": {
"title": "Enterprise repository",
"rationale": "Riferimenti a `enterprise.proxmox.com` in `/etc/apt/sources.list` e `sources.list.d`, rispetto allo stato restituito da `pvesubscription get`.",
"summary": {
"notEnabled": "The enterprise repository is not enabled",
"subscribed": "The enterprise repository is backed by a subscription",
"unsubscribed": "The enterprise repository is enabled without an active subscription",
"evaluationFailed": "The check could not be evaluated"
"notEnabled": "Il repository enterprise non è abilitato",
"subscribed": "Il repository enterprise è supportato da un abbonamento",
"unsubscribed": "Il repository enterprise è abilitato senza un abbonamento attivo",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"memory_overcommit": {
"title": "Memory allocation",
"rationale": "Il tetto `memory` di ogni configurazione guest rispetto a MemTotal, con i guest in esecuzione conteggiati a parte. I container consumano fino a quel limite; le macchine virtuali senza ballooning lo riservano.",
"summary": {
"withinRatio": "Guests are allocated {percent}% of host memory",
"aboveRatio": "Guests are allocated {percent}% of host memory",
"evaluationFailed": "The check could not be evaluated"
"withinRatio": "Gli ospiti sono assegnati {percent}% della memoria host",
"aboveRatio": "Gli ospiti sono assegnati {percent}% della memoria host",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"time_synchronisation": {
"title": "Time synchronisation",
"rationale": "NTP e NTPSynchronized come li riporta `timedatectl`. L'appartenenza al cluster, la validazione dei certificati e l'ordine dei log dipendono da orologi concordi. Un altro meccanismo può regolare l'orologio.",
"summary": {
"synchronised": "The clock is synchronised with a time source",
"disabled": "Time synchronisation is disabled",
"notSynchronised": "Time synchronisation is enabled but the clock is not synchronised",
"evaluationFailed": "The check could not be evaluated"
"synchronised": "L'orologio è sincronizzato con una fonte di tempo",
"notSynchronised": "La sincronizzazione del tempo è abilitata ma l'orologio non è sincronizzato",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"kernel_current": {
@@ -5420,21 +5426,21 @@
"rationale": "Il kernel in esecuzione rispetto a quello che l'host avvierebbe successivamente, come lo riporta `proxmox-boot-tool`. Un kernel più recente solo installato può essere trattenuto di proposito; una differenza dopo un riavvio indica un avvio che non ha avuto effetto.",
"summary": {
"current": "Il kernel in esecuzione {version} è quello che l'host avvierebbe successivamente",
"newerAvailable": "The host runs {running} while {newest} is installed",
"newerSelected": "L'host esegue {running} e avvierebbe {selected} al prossimo riavvio",
"wouldDowngrade": "L'host esegue {running} ma avvierebbe il più vecchio {selected} al prossimo riavvio",
"bootTargetUnknown": "L'host esegue {version}; non è stato possibile leggere il kernel scelto per il prossimo avvio",
"evaluationFailed": "The check could not be evaluated"
"newerAvailable": "L'host esegue {running} mentre {newest} è installato",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"security_updates": {
"title": "Security updates",
"rationale": "Pacchetti in sospeso la cui origine è un repository di sicurezza, da un `apt-get upgrade` simulato. Il numero riflette quanto riporta apt, non la gravità di ciò che ogni pacchetto corregge.",
"summary": {
"none": "No package updates are pending",
"noSecurity": "{total} update(s) pending, none from a security repository",
"pending": "{count} of {total} pending update(s) come from a security repository",
"evaluationFailed": "The check could not be evaluated"
"none": "Nessun aggiornamento dei pacchetti sono in sospeso",
"noSecurity": "{total} update(s) in attesa, nessuno da un repository di sicurezza",
"pending": "{count} di {total} in attesa di aggiornamenti(i) provengono da un repository di sicurezza",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"journal_size": {
@@ -5442,10 +5448,10 @@
"rationale": "Il journal su disco rispetto al tetto che gli si applica: SystemMaxUse quando è impostato, altrimenti il predefinito di journald, un decimo del filesystem su cui risiede.",
"summary": {
"bounded": "Il journal occupa {size}, entro il suo tetto effettivo",
"large": "The journal holds {size} on disk",
"nearCap": "Il journal occupa {size} ed è al {percent}% del suo tetto effettivo",
"capUnknown": "Il journal occupa {size}; non è stato possibile determinarne il tetto effettivo",
"evaluationFailed": "The check could not be evaluated"
"large": "La rivista contiene {size} su disco",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"swap_configured": {
@@ -5454,7 +5460,7 @@
"summary": {
"active": "{size} of swap is active",
"none": "No swap area is active",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"filesystem_capacity": {
@@ -5500,7 +5506,7 @@
"summary": {
"synchronised": "Le {total} partizioni di avvio portano gli stessi kernel",
"attention": "{count} di {total} partizioni di avvio richiedono una revisione",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
},
"rationale": "I report sulle partizioni di sistema EFI proxmox-boot-tool e i kernel che ciascuna trasporta. Lo stato proxmox-boot-tool può montare temporaneamente partizioni di sistema EFI; non viene tentato alcun avvio."
},
@@ -5510,7 +5516,7 @@
"summary": {
"allRunning": "I {total} servizi essenziali sono attivi e nessuna unità è fallita",
"attention": "{count} rilievo/i tra le unità",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"ha_state": {
@@ -5519,7 +5525,7 @@
"summary": {
"managed": "I {total} servizi gestiti sono in uno stato assestato su {nodes} nodo/i",
"attention": "{count} rilievo/i su {total} servizio/i gestito/i",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
}
},
@@ -5528,27 +5534,27 @@
"title": "Container privileges",
"rationale": "L'impostazione `unprivileged` di ogni configurazione container. La sua assenza significa che il container condivide lo spazio dei nomi utente dell'host, cosa che alcuni carichi richiedono.",
"summary": {
"allUnprivileged": "All {total} containers are unprivileged",
"privileged": "{count} of {total} containers run privileged",
"evaluationFailed": "The check could not be evaluated"
"allUnprivileged": "Tutti i contenitori {total} non sono privati",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"qemu_without_agent": {
"title": "Guest agent on virtual machines",
"rationale": "L'impostazione `agent` nella configurazione di ogni macchina virtuale. L'impostazione indica che l'agente è dichiarato, non che risponda.",
"summary": {
"allHaveAgent": "All {total} virtual machines declare the guest agent",
"missingAgent": "{count} of {total} virtual machines do not declare the guest agent",
"evaluationFailed": "The check could not be evaluated"
}
"allHaveAgent": "Tutte le macchine virtuali {total} dichiarano l'agente ospite",
"missingAgent": "{count} delle macchine virtuali {total} non dichiarano l'agente ospite",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
},
"title": "Agente ospite su macchine virtuali"
},
"autostart": {
"title": "Automatic start",
"rationale": "L'impostazione `onboot` di ogni guest, esclusi i template e i guest gestiti da HA. Se un guest debba tornare da solo lo indica la politica dichiarata.",
"summary": {
"allAutostart": "All {total} guests start with the host",
"notAutostart": "{count} of {total} guests do not start with the host",
"evaluationFailed": "The check could not be evaluated"
"allAutostart": "Tutti gli ospiti {total} iniziano con l'host",
"notAutostart": "{count} degli ospiti di {total} non iniziano con l'host",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"stuck_snapshots": {
@@ -5556,28 +5562,28 @@
"rationale": "Stato e anzianità degli snapshot e attività in corso. Un'operazione recente o senza data verificabile non è considerata interrotta.",
"summary": {
"noSnapshots": "No guest holds snapshots",
"allComplete": "The {total} snapshot(s) are complete",
"stuck": "{count} of {total} snapshot(s) were left mid-operation",
"evaluationFailed": "The check could not be evaluated"
"allComplete": "Le snapshot {total} sono complete",
"stuck": "{count} di {total} snapshot(s) sono stati lasciati a metà operazione",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"cpu_host_type": {
"title": "Virtual CPU model",
"rationale": "Il valore `cpu` di ogni macchina virtuale. `host` espone il set di istruzioni del processore fisico, il che limita i nodi verso cui il guest può migrare. L'incompatibilità con una destinazione precisa non viene determinata qui.",
"summary": {
"none": "None of the {total} virtual machines is pinned to the host processor",
"pinned": "{count} of {total} virtual machines are pinned to the host processor",
"evaluationFailed": "The check could not be evaluated"
"none": "Nessuna delle macchine virtuali {total} è fissata al processore host",
"pinned": "Le macchine virtuali {count} di {total} sono fissate al processore host",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"replication_state": {
"title": "Replication",
"rationale": "Processi di replica dalle API: numero di errori, ultimo errore, ultima sincronizzazione e il calendario dichiarato da ciascun processo. I processi in pausa sono indicati come tali.",
"summary": {
"healthy": "The {total} replication job(s) report no error",
"failing": "{count} of {total} replication job(s) report an error",
"statusUnavailable": "Replication jobs are defined but their status could not be read",
"evaluationFailed": "The check could not be evaluated"
"healthy": "Il lavoro di replica {total} non segnala errore",
"failing": "{count} del lavoro di replica {total} segnala un errore",
"statusUnavailable": "I lavori di replica sono definiti ma il loro stato non può essere letto",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
}
},
@@ -5589,39 +5595,39 @@
"bothEnabled": "L'attivazione del firewall è configurata a livello di datacenter e nodo",
"datacenterOff": "Il firewall è disattivato a livello di datacenter, quindi le regole del nodo non vengono applicate",
"nodeOff": "Il firewall è attivato a livello di datacenter ma non su questo nodo",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"lynis_warnings": {
"title": "Lynis warnings",
"rationale": "Avvisi dell'ultimo audit di Lynis, ciascuno con il proprio identificatore di test, e l'età di quell'audit. Un audit viene eseguito solo se Lynis è installato e non esiste alcun report completo. I suggerimenti non sono inclusi.",
"summary": {
"none": "The last Lynis audit recorded no warnings",
"found": "The last Lynis audit recorded {count} warning(s)",
"incomplete": "The Lynis report is incomplete",
"evaluationFailed": "The check could not be evaluated",
"noneStale": "L'ultimo audit di Lynis non ha registrato avvisi, e il suo report ha {days} giorno/i",
"foundStale": "L'ultimo audit di Lynis ha registrato {count} avviso/i, e il suo report ha {days} giorno/i"
"foundStale": "L'ultimo audit di Lynis ha registrato {count} avviso/i, e il suo report ha {days} giorno/i",
"none": "L'ultima verifica di Lynis non ha registrato avvisi",
"found": "L'ultimo controllo di Lynis ha registrato {count} avvisi(i)",
"incomplete": "La relazione Lynis è incompleta",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"certificate_expiry": {
"title": "Certificate validity",
"rationale": "La data di scadenza del certificato che pveproxy serve da /etc/pve/local. Un certificato personalizzato ha la precedenza su quello generato da Proxmox.",
"summary": {
"valid": "The certificate is valid for {days} more day(s)",
"expiring": "The certificate expires in {days} day(s)",
"expired": "The certificate expired {days} day(s) ago",
"evaluationFailed": "The check could not be evaluated"
"valid": "Il certificato è valido per {days} più giorno(i)",
"expiring": "Il certificato scade in {days} day(s)",
"expired": "Il certificato è scaduto {days} giorno(i) fa",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"ssh_root_login": {
"title": "SSH root access",
"rationale": "PermitRootLogin nella configurazione effettiva di `sshd -T`, con i metodi di autenticazione a cui si combina. Proxmox viene consegnato con `yes`, che accetta una password.",
"summary": {
"password": "Root may sign in over SSH with a password",
"keyOnly": "Root may sign in over SSH with a key only",
"denied": "Root may not sign in over SSH",
"evaluationFailed": "The check could not be evaluated"
"password": "Root può accedere a SSH con una password",
"keyOnly": "Root può firmare in oltre SSH con una chiave solo",
"denied": "La radice non può firmare su SSH",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
}
},
@@ -5630,9 +5636,9 @@
"title": "Volume assignment",
"rationale": "Volumi dei guest sullo storage locale rispetto ai riferimenti nelle configurazioni correnti, in sospeso e di snapshot. Backup, ISO e template restano fuori dal confronto. Un volume senza riferimento è un candidato alla verifica.",
"summary": {
"none": "No orphaned volumes were found",
"found": "{count} volumi senza riferimenti nelle configurazioni esaminate",
"evaluationFailed": "The check could not be evaluated"
"none": "Non sono stati trovati volumi orfani",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"zfs_arc_max": {
@@ -5640,21 +5646,21 @@
"rationale": "Valori effettivi di c_min, c_max e dimensione dell'ARC, il parametro del modulo caricato e le impostazioni persistenti in /etc/modprobe.d. Un valore configurato a zero seleziona il predefinito del modulo; l'ARC è un tetto e la memoria che occupa è recuperabile.",
"summary": {
"bounded": "Il limite dell'ARC è il {percent}% della memoria dell'host, e la memoria che occupa è recuperabile",
"high": "The ARC may use {percent}% of host memory",
"unset": "The ARC has no explicit limit set",
"conflicting": "{count} impostazioni dell'ARC non concordano tra loro",
"pending": "Un'impostazione persistente dell'ARC differisce dal valore del modulo in esecuzione",
"evaluationFailed": "The check could not be evaluated"
"high": "L'ARC può utilizzare {percent}% della memoria host",
"unset": "L'ARC non ha un limite esplicito",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"zfs_scrub_age": {
"title": "ZFS scrub",
"rationale": "L'ultimo scrub completato registrato da `zpool status` per ogni pool. Un resilver non è uno scrub. Un pool creato di recente non ha ancora avuto occasione di eseguirne uno.",
"summary": {
"recent": "The {total} pool(s) were scrubbed within the last 35 days",
"overdue": "{count} of {total} pool(s) have not been scrubbed in 35 days",
"neverScrubbed": "{count} pool(s) record no scrub",
"evaluationFailed": "The check could not be evaluated"
"recent": "La piscina {total} è stata lavata negli ultimi 35 giorni",
"overdue": "{count} della piscina {total} non sono stati ripuliti in 35 giorni",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"thin_pool_overprovisioning": {
@@ -5664,7 +5670,7 @@
"withinRatio": "I {total} thin pool sono al di sotto delle soglie di verifica applicate",
"aboveRatio": "{count} thin pool su {total} distribuiscono più capacità di quella che possiedono",
"pressure": "{pressure} thin pool su {total} sono vicini a riempire dati o metadati",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"connected_storage": {
@@ -5673,7 +5679,7 @@
"summary": {
"available": "PVE indica tutti i {total} archivi come disponibili; i componenti interni remoti e l'accesso in scrittura non sono stati verificati",
"attention": "{count} archivi su {total} richiedono una verifica",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"pool_integrity": {
@@ -5691,7 +5697,7 @@
"summary": {
"healthy": "Ceph riporta HEALTH_OK",
"degraded": "Ceph riporta {state}, con {count} controllo/i nominato/i",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"array_integrity": {
@@ -5700,7 +5706,7 @@
"summary": {
"intact": "I {total} array e mappe conservano la loro ridondanza",
"degraded": "{count} di {total} array o mappe ne sono a corto",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
}
},
@@ -5712,7 +5718,7 @@
"withinLife": "Le {total} letture dei dischi non superano la soglia indicativa di cinque anni",
"pastLife": "{count} di {total} letture dei dischi superano cinque anni di servizio",
"noReadings": "Nessun disco espone contatori SMART utilizzabili ({skipped} senza letture)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"disk_errors": {
@@ -5729,23 +5735,22 @@
"title": "Bond members",
"rationale": "Lo stato MII di ogni membro del bond da /proc/net/bonding e quanti collegamenti restano. In active-backup un membro di riserva risulta attivo e non trasporta traffico.",
"summary": {
"allUp": "All members of the {total} bond(s) are up",
"membersDown": "{count} bond member(s) are not up",
"evaluationFailed": "The check could not be evaluated"
"allUp": "Tutti i membri delle obbligazioni {total} sono in aumento",
"membersDown": "I membri delle obbligazioni {count} non sono aggiornati",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
},
"bridge_without_ports": {
"title": "Bridge ports",
"rationale": "La configurazione delle porte di ogni bridge. Un bridge senza porta fisica serve una rete interna o instradata.",
"summary": {
"allConnected": "The {total} bridge(s) carry a port",
"isolated": "{count} of {total} bridge(s) carry no port",
"evaluationFailed": "The check could not be evaluated"
"allConnected": "Il ponte {total} porta una porta",
"evaluationFailed": "Il controllo non potrebbe essere valutato"
}
}
}
},
"summaryFallback": "The check could not be evaluated",
"acceptRisk": {
"action": "Accetta il rischio",
"revoke": "Riporta tra gli attivi",
@@ -5779,17 +5784,15 @@
"OK": "OK"
},
"viewSwitch": {
"ariaLabel": "Switch between assessment and inventory",
"assessment": "Assessment",
"inventory": "Inventory",
"policy": "Politica",
"changes": "Modifiche"
"changes": "Modifiche",
"ariaLabel": "Interruttore tra valutazione e inventario"
},
"inventory": {
"loading": "Loading inventory…",
"failed": "The inventory could not be composed.",
"collectedAt": "Composed on {when}",
"unavailable": "Not read in this inventory",
"unresolved": "path not resolved",
"noUplink": "no uplink",
"identity": "Node identity",
@@ -5832,12 +5835,10 @@
"protection": "Backup",
"passthrough": "Passthrough",
"noBackup": "No backup",
"noBackupDetail": "No enabled backup job selects this guest.",
"applications": "Applications",
"versionUnknown": "version not detected",
"passthroughTitle": "PCI passthrough",
"iommuGroup": "IOMMU group {group}",
"sharedGroup": "{count} more device(s) in the same group",
"proxmenux": "ProxMenux optimizations",
"latency": "Latenza di rete",
"subscriptionStatus": {
@@ -5848,7 +5849,11 @@
"suspended": "Sospeso",
"new": "In attesa di attivazione",
"unknown": "Sconosciuto"
}
},
"failed": "L'inventario non poteva essere composto.",
"unavailable": "Non leggere in questo inventario",
"noBackupDetail": "Nessun lavoro di backup abilitato seleziona questo guest.",
"sharedGroup": "{count} più dispositivo(i) nello stesso gruppo"
},
"profile": {
"label": "Report",
@@ -5871,7 +5876,6 @@
"area": "Area",
"inventoryAnnex": "Inventory annex",
"scopeTitle": "Scope of this report",
"scopeBody": "This report describes the Proxmox VE node named above, as observed from the node itself at the time stated. It does not cover the interior of the guests beyond what they declare, network equipment outside the host, physical infrastructure, or any dependency not visible from this node. Findings marked as not determined were not measured and are not evidence of absence.",
"building": "Composizione del rapporto…",
"node": "Nodo",
"profile": "Profilo",
@@ -6020,11 +6024,12 @@
"structureSubtitle": "Com'è costruito e configurato {node}",
"postureTitle": "Posture",
"posture": {
"security": "Host exposure and access.",
"backup": "Guest protection and whether it is real.",
"capacity": "Room to grow and the wear on the disks."
"security": "Esposizione e accesso host.",
"backup": "Protezione degli ospiti e se è reale.",
"capacity": "Stanza per crescere e l'usura sui dischi."
},
"scopeReadOnly": "La valutazione esamina le impostazioni e l'integrità dell'host. Può scrivere report e log; i controlli dello stato di avvio possono montare temporaneamente partizioni di sistema EFI."
"scopeReadOnly": "La valutazione esamina le impostazioni e l'integrità dell'host. Può scrivere report e log; i controlli dello stato di avvio possono montare temporaneamente partizioni di sistema EFI.",
"scopeBody": "Questo rapporto descrive il nodo VE Proxmox sopra indicato, come osservato dal nodo stesso al momento indicato. Non copre l'interno degli ospiti al di là di quanto dichiarano, apparecchiature di rete al di fuori dell'host, infrastrutture fisiche, o qualsiasi dipendenza non visibile da questo nodo. I risultati contrassegnati come non determinati non sono stati misurati e non sono prove di assenza."
},
"results": "Risultati",
"classifications": {
@@ -6178,15 +6183,19 @@
"notApplicableScope": "Nulla nell'ambito esaminato a cui questo controllo si applichi.",
"lynis": {
"title": "Run Lynis",
"bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.",
"bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.",
"withLynis": "Run with Lynis",
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
"cancel": "Cancel",
"bodyNotRun": "Lynis è installato ma non è stato ancora eseguito. L'esecuzione ora completa la revisione di sicurezza, ma il processo può richiedere alcuni minuti.",
"bodyStale": "Il rapporto Lynis è {days} giorni. È possibile eseguire ora per aggiornare i dati (ci vuole un po 'di più) o continuare con il rapporto esistente."
},
"readOnlyNotice": "La valutazione esamina le impostazioni e l'integrità dell'host. Può scrivere report e log; i controlli dello stato di avvio possono montare temporaneamente partizioni di sistema EFI.",
"noActionNeeded": "Niente da fare. Questo assegno ha trovato quello che si aspetta di trovare.",
"couldNotEvaluate": "Questo controllo non può essere valutato, quindi non segnala nulla in entrambi i casi. Le prove riportano ciò che non era in grado di leggere."
"couldNotEvaluate": "Questo controllo non può essere valutato, quindi non segnala nulla in entrambi i casi. Le prove riportano ciò che non era in grado di leggere.",
"neverRun": "Questo host non è stato ancora valutato.",
"noFindings": "Nessun risultato corrisponde al filtro corrente.",
"acceptedNotice": "{count} ha accettato i rischi registrati su questo host.",
"summaryFallback": "Il controllo non potrebbe essere valutato"
},
"runtime": {
"notifications": {
+145 -136
View File
@@ -1487,7 +1487,6 @@
"containerNameLabel": "Nome do contêiner",
"containerNamePlaceholder": "por exemplo, <seu-nome-do-contêiner>",
"ociLabelKeyLabel": "Chave de rótulo OCI",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Caminho do interpretador Python",
"pythonInterpreterPlaceholder": "por exemplo, /opt/<seu-app>/venv/bin/python",
"pipDistLabel": "Nome da distribuição (pacote pip)",
@@ -1534,11 +1533,8 @@
"portHttps": "https",
"portLogoLabel": "URL do logotipo deste link (opcional)",
"portLogoPlaceholder": "por exemplo, https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
"portCategoryNone": "No category",
"portCategoryAddNew": "+ Add new category…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
"removePortTooltip": "Remover porta",
"detectMethodDpkg": "dpkg ·",
"detectMethodApk": "APK ·",
@@ -1631,7 +1627,11 @@
"notifyUpstreamLabel": "Notifique-me quando uma nova versão upstream estiver disponível",
"notifyUpstreamHelp": "Envia `app_update_available` para os canais habilitados em Configurações → Notificações. Desligue se este aplicativo não puder ser atualizado em sua caixa.",
"excludeFromBadgeLabel": "Excluir do contador de atualizações LXC",
"excludeFromBadgeHelp": "não conte este aplicativo no selo de atualizações agregadas no cartão de lista LXC.Útil quando você está fixado em uma versão específica propositalmente (requisito do rastreador, congelamento de compatibilidade).Não afeta o próprio estado da guia Aplicativo ou a notificação de saída."
"excludeFromBadgeHelp": "não conte este aplicativo no selo de atualizações agregadas no cartão de lista LXC.Útil quando você está fixado em uma versão específica propositalmente (requisito do rastreador, congelamento de compatibilidade).Não afeta o próprio estado da guia Aplicativo ou a notificação de saída.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"portCategoryPlaceholder": "Categoria para o painel de aplicativos (opcional)",
"portCategoryCustomPlaceholder": "Digite uma categoria e pressione Enter (Esc para cancelar)",
"portCustomUrlPlaceholder": "URL personalizada (por exemplo, https://vault.example.com) — substitui IP:port"
},
"statusFilter": {
"ariaLabel": "Filtrar máquinas virtuais e contêineres",
@@ -2515,10 +2515,10 @@
"scope": {
"label": "Token permissions",
"readOnly": "Read-only",
"readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.",
"fullAdmin": "Full admin",
"fullAdminHint": "Full control, like your own session.",
"fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations."
"readOnlyHint": "Lê métricas e status. Recomendado para painéis e integrações.",
"fullAdminHint": "Controle total, como a sua própria sessão.",
"fullAdminWarning": "Um token de administração completa pode fazer tudo que puder: desligar ou reiniciar o host, executar atualizações e abrir um terminal. Compartilhe-o apenas com integrações totalmente confiáveis."
}
},
"firewall": {
@@ -3302,26 +3302,33 @@
"gotIt": "Entendi!",
"dontShowAgain": "Não mostrar novamente para esta versão",
"currentFeatures": {
"appDetection": "Smarter app detection in the App tab: Docker is correctly promoted as the parent workload during cold start (Portainer/SearXNG no longer briefly show up as native apps), unregistered suggestions live in the startup cache, and 'Find applications' runs a fresh catalog-backed scan on demand.",
"dockerUpdates": "The Updates tab now covers Docker end-to-end: Docker Engine and per-image update tracking follow the same 24-hour rolling cycle as OS packages, with a 'Check now' action for on-demand digest comparison — no more waiting for the daily collector.",
"appCatalog": "New application detection catalog with over 380 tracked workloads, generated live from community-scripts across seven detector methods (file, binary, dpkg, apk, Python, Docker exec, Docker label). Primary and fallback detectors cover both new and historical LXC layouts.",
"pushover": "Pushover joins Telegram, Gotify, Discord, Email and Apprise as a native notification channel — user/API key, device and sound selectors, priority 0 for regular messages, optional priority 1 for CRITICAL events. Suggested by @benginx (#308).",
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
"auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
"changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"appDetection": "Detecção de aplicativos mais inteligente na aba App: Docker é corretamente promovido como a carga de trabalho pai durante o início frio (Portainer/SearXNG já não aparece brevemente como aplicativos nativos), sugestões não registradas ao vivo no cache de inicialização, e 'Find applications' executa uma nova varredura baseada em catálogo sob demanda.",
"dockerUpdates": "A página Atualizações agora cobre o Docker de ponta a ponta: Docker O rastreamento de atualização do motor e por imagem segue o mesmo ciclo de rolamento de 24 horas que os pacotes OS, com uma ação 'Check now' para comparação de digest sob demanda – sem mais esperar pelo coletor diário.",
"appCatalog": "Novo catálogo de detecção de aplicativos com mais de 380 cargas de trabalho rastreadas, geradas ao vivo a partir de scripts comunitários em sete métodos de detector (ficheiro, binário, dpkg, apk, Python, Docker exec, etiqueta Docker). Os detectores primários e de retorno cobrem layouts LXC novos e históricos.",
"pushover": "Pushover junta-se ao Telegram, Gotify, Discord, Email e Apprise como um canal de notificação nativo — chave de usuário/API, seletores de som e dispositivo, prioridade 0 para mensagens regulares, prioridade opcional 1 para eventos CRITICAIS. Sugerido por @benginx (# 308).",
"appsDashboard": "Novo painel de topo de Apps — um único lançador para cada Web Link em todo o nó. Aplicativos registrados em LXC e Web personalizada definida pelo usuário Links compartilham a mesma grade com crachás de categoria, pesquisa e links profundos de um clique de volta para o modal convidado.",
"lxcAppsUpdates": "A guia App dentro de cada LXC registra aplicativos instalados, captura weblinks e rastreia versões upstream. A guia Atualizações retrabalhadas aplica pacotes OS e atualizações de aplicativos a partir de um único botão; o motor Docker e o rastreamento por imagem seguem o mesmo ciclo de 24 horas, com uma ação 'Cheque agora' sob demanda.",
"multilingual": "O Monitor agora fala 8 línguas: inglês, espanhol, alemão, francês, italiano, português, sueco e eslovaco. Enormes graças a @vaso73 por construir o andaime i18n que tornou isso possível.",
"nvidiaMultiGpu": "O ciclo de vida do driver NVIDIA se move para a propriedade por BDF para que um host multi-GPU possa passar um cartão para um VM e manter o outro operacional no host ou em LXCs, além de um kernel + branch + GPU-aware version picker (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute e qualquer proxy self-hosted em IPs privados, loopback ou Docker redes são reconhecidas ao carregar o catálogo do modelo. A superfície dropdown do erro do servidor (ou a razão de rede subjacente) diretamente sob o botão Carregar (#325, relatado por @jorgeffonte).",
"secureGatewayArch": "Assistente de Gateway seguro — o download do modelo alpino, seleção de modelos local e pct create todos correspondem à arquitetura real do host, então os hosts x86 64 recebem containers amd64 e os hosts arm64 recebem containers arm64 (#324, reportados por @N0X4DD0).",
"atomicNotifications": "Eventos de notificação reservam sua impressão digital de deduplicação atomicamente antes do processamento de IA e entrega de canais, então coletores simultâneos, callbacks de conclusão ou processos de monitoramento paralelos não podem enviar o mesmo evento duas vezes. A reserva é liberada quando nenhum canal tem sucesso, preservando repetições.",
"borgSshPort": "Alvo remoto Borg — a janela Adicionar destino Borg e o shell TUI aceitam uma porta SSH personalizada. BORG RSH, o fluxo de instalação da chave automática e a sonda de capacidade todos honram. Totalmente compatível com entradas existentes criadas sem uma porta explícita (sugerida por @songochain na discussão #236).",
"githubToken": "Configurações → GitHub API aceita um token de acesso pessoal opcional para liberação e verificação de tags quando a quota anônima está esgotada. O token é criptografado em repouso e nunca retorna ao navegador; o erro de limite de taxa é traduzido em cada idioma Monitor (sugerido por @SystemIdleProcess em discussão #306).",
"replicationContext": "As notificações de falha de replicação nativa do Proxmox resolvem o ID do trabalho de replicação, afeta o ID do VM/LXC e o nome do convidado; o bloco de erro exato do Proxmox é preservado como razão, e cada tarefa de replicação deduplica-se independentemente (referido por Ale R.).",
"auditAssessment": "Auditoria & Relatório — uma nova página que documenta e avalia o nó. A Avaliação realiza um catálogo de verificações em segurança, backups, capacidade, armazenamento, rede, hardware, convidados e sistema, classificando cada achado como crítico, alerta, observação ou conforme e declarando o que leu em vez de julgá-lo.",
"changeJournal": "Change journal — uma visão de mudanças que lista exatamente o que ProxMenux modificou nesta máquina: cada arquivo de configuração, pacote e serviço que tocou, com o estado anterior de cada um, deduplicado para uma visão de estado atual que mostra o host como ele está agora em vez de um log de cada execução.",
"auditReports": "Relatórios – uma auditoria completa mais revisão de segurança focada, garantia de backup e relatórios de capacidade e desgaste, cada abertura em seu próprio cabeçalho postura, e um Inventário imprimível; todas as exportações para PDF. Uma avaliação de segurança pede antes de executar Lynis para que uma corrida seja rápida.",
"auditPolicyBaseline": "Política e linha de base — declarar o que o host deve ser (requisitos de backup, firewall, login do root SSH) para que as descobertas classem contra ele, marque uma corrida como referência e veja o que mudou desde, com novas descobertas resolvidas e aceitas mantidas separadas.",
"groupedAppUpdates": "Notificações de atualização de aplicativos agrupadas — uma mensagem completa por digitalização em vez de uma por aplicação, agrupadas por LXC com as versões instaladas e disponíveis.",
"adminTokenScope": "Âmbito administrativo — abrir um terminal Monitor e desativar a autenticação agora requer um token de administração completa, então um token API somente para leitura emitido para uma integração de monitoramento permanece somente para leitura (referido por @f3rs3n).",
"ociAppTab": "Contentores OCI no separador App — um contentor instalado pelo OCI manager Apps é reconhecido a partir do seu registo de instalação: a aplicação e a sua imagem, uma nova imagem detetada por digest e uma ligação ao repositório da imagem.",
"ociUpdatesTab": "Atualizações de contentores OCI — Atualizar e Recriar abrem o mesmo percurso do menu OCI, o backup feito antes da atualização pode ser mantido num armazenamento de backups e a imagem pode ser atualizada de forma agendada.",
"ociLogsTab": "Separador Logs para contentores OCI — a saída da consola da aplicação, guardada no host e acompanhada em direto, com filtro e transferência. A consola do Proxmox destes contentores abre uma shell.",
"appsUpdateShortcut": "O ícone de atualização da página Apps abre o contentor diretamente no seu separador Atualizações.",
"webhookHttps": "As notificações do Proxmox chegam ao Monitor com HTTPS ativo — são entregues num endereço apenas local e já não falham por um erro de certificado.",
"persistentLogs": "Uma rajada de erros nos logs que terminou já não é apresentada como persistente: um padrão tem de continuar a aparecer durante 15 minutos, e o seu aviso fecha-se sozinho (reportado por @Joshua1264).",
"mountsLanAddress": "Os pontos de montagem em LVM-thin e outros armazenamentos em bloco mostram a sua utilização, e as aplicações com vários contentores abrem no seu endereço LAN."
}
},
"network": {
@@ -5027,23 +5034,20 @@
"uncategorized": "Uncategorized",
"openAriaLabel": "Open {name} in a new tab",
"openGuestAriaLabel": "Abrir {name} ({type} {id})",
"emptyTitle": "No apps with a web link yet.",
"emptyHint": "Register a Web Link for any app (App tab of a CT) to see it here.",
"customLinkAdd": "Add link",
"editModeToggle": "Edit",
"editModeDone": "Done",
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Editar o link personalizado {name}",
"openUpdatesAria": "Abrir o separador Atualizações de {name}",
"openUpdatesTitle": "Atualização disponível — abrir o separador Atualizações",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
"customLinkLogo": "Logo URL (optional)",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkCategory": "Category (optional)",
"customLinkBinding": "Bound to",
"customLinkBindingNone": "Not bound to any guest",
"customLinkBindingHelp": "Bind this link to a VM or CT to add its ID + name to the card and jump to the guest with one click.",
"customLinkSave": "Save",
"customLinkCreate": "Create",
"customLinkCancel": "Cancel",
@@ -5079,7 +5083,12 @@
"remoteAccessVpn": "Remote Access & VPN",
"webserversProxies": "Webservers & Proxies",
"zigbeeZwaveMatter": "ZigBee, Z-Wave & Matter"
}
},
"emptyTitle": "Nenhum aplicativo com um link web ainda.",
"emptyHint": "Registre um Web Link para qualquer aplicativo (App tab de um CT) para vê-lo aqui.",
"customLinkLogoPlaceholder": "Por exemplo, https://exemplo.com/logo.webp",
"customLinkBindingNone": "Não está ligado a nenhum hóspede.",
"customLinkBindingHelp": "Ligar este link a um VM ou CT para adicionar o seu ID + nome ao cartão e saltar para o hóspede com um clique."
},
"audit": {
"presentation": {
@@ -5234,13 +5243,10 @@
"loading": "Loading assessment…",
"run": "Run assessment",
"running": "Assessing…",
"neverRun": "This host has not been assessed yet.",
"lastRun": "Last assessed on {when}",
"stale": "{days} days ago",
"unverifiedChecks": "Não obtidas: {checks}. Cada uma diz na sua evidência o que não conseguiu ler.",
"noFindings": "No findings match the current filter.",
"affectedCount": "{count} affected",
"acceptedNotice": "{count} accepted risk(s) recorded on this host.",
"reviewOn": "Rever em: {when}",
"reviewDue": "A rever — a decisão continua em vigor",
"reviewDueNotice": "{count} decisão(ões) aceite(s) aguardam revisão.",
@@ -5272,7 +5278,7 @@
"sources": "Fontes e datas de coleta"
},
"errors": {
"runFailed": "The assessment could not be started."
"runFailed": "A avaliação não pôde ser iniciada."
},
"checks": {
"backup": {
@@ -5285,7 +5291,7 @@
"uncovered": "{count} de {total} convidados não são selecionados por nenhuma tarefa de backup ativa",
"excludedData": "Há {count} exclusões de discos ou montagens para revisar",
"uncoveredExpected": "{required} convidados declarados como necessitando backup não são selecionados por nenhuma tarefa ativa",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
},
"nextStep": {
"noJobs": "Crie uma tarefa de backup neste nó e selecione os convidados segurando dados que você não gostaria de reconstruir à mão. Uma tarefa que existe mas está desactivada não seleciona nada.",
@@ -5300,10 +5306,10 @@
"rationale": "Idade: tempo decorrido desde a última cópia armazenada. Limite utilizado: idade de referência com a qual essa cópia é comparada.",
"summary": {
"recent": "As {total} verificações de máquina/destino cumprem o critério de antiguidade indicado",
"stale": "{count} of {total} guests have no backup from the last 30 days",
"noBackups": "No stored backup matches a guest on this node",
"attention": "{count} de {total} verificações de máquina/destino requerem revisão",
"evaluationFailed": "The check could not be evaluated"
"stale": "{count} dos hóspedes do {total} não têm backup dos últimos 30 dias",
"noBackups": "Nenhuma cópia de segurança armazenada corresponde a um convidado neste nó",
"evaluationFailed": "A verificação não pôde ser avaliada"
},
"nextStep": {
"stale": "Descubra por que o trabalho parou de produzir cópias para esses hóspedes: ele pode ter sido desativado, sua programação pode nunca disparar, ou suas corridas podem estar falhando. A verificação de resultados de execução relata como cada trabalho terminou pela última vez.",
@@ -5317,10 +5323,10 @@
"rationale": "A retenção tal como o Proxmox a resolve: definição da tarefa, depois do armazenamento, depois o valor por omissão do nó. A retenção aplicada por um servidor de backup não é legível a partir deste nó.",
"summary": {
"allDefined": "As {total} tarefas resolvem uma definição de retenção",
"missing": "{count} of {total} enabled job(s) declare no retention",
"notDeclared": "{count} de {total} tarefas guardam todas as cópias: não têm retenção declarada",
"onServer": "{count} de {total} tarefas escrevem num servidor de backup, que as poda com as suas próprias tarefas",
"evaluationFailed": "The check could not be evaluated"
"missing": "{count} do( s) trabalho( s) habilitado( s) {total} declaram não haver retenção",
"evaluationFailed": "A verificação não pôde ser avaliada"
},
"nextStep": {
"missing": "Defina uma retenção nesses trabalhos, ou no armazenamento para o qual eles escrevem. Proxmox toma a configuração do trabalho primeiro, depois o armazenamento, depois o padrão do nó.",
@@ -5383,36 +5389,36 @@
"summary": {
"none": "Nada solicitou um reinício",
"pending": "{count} elementos estão instalados e aguardam um reinício",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"enterprise_repo_without_subscription": {
"title": "Enterprise repository",
"rationale": "Referências a `enterprise.proxmox.com` em `/etc/apt/sources.list` e `sources.list.d`, face ao estado devolvido por `pvesubscription get`.",
"summary": {
"notEnabled": "The enterprise repository is not enabled",
"subscribed": "The enterprise repository is backed by a subscription",
"unsubscribed": "The enterprise repository is enabled without an active subscription",
"evaluationFailed": "The check could not be evaluated"
"notEnabled": "O repositório da empresa não está habilitado",
"subscribed": "O repositório da empresa é suportado por uma assinatura",
"unsubscribed": "O repositório corporativo está habilitado sem uma assinatura ativa",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"memory_overcommit": {
"title": "Memory allocation",
"rationale": "O teto `memory` de cada configuração de convidado face a MemTotal, com os convidados em execução contados à parte. Os contentores consomem até esse limite; as máquinas virtuais sem ballooning reservam-no.",
"summary": {
"withinRatio": "Guests are allocated {percent}% of host memory",
"aboveRatio": "Guests are allocated {percent}% of host memory",
"evaluationFailed": "The check could not be evaluated"
"withinRatio": "Os hóspedes recebem {percent}% da memória da máquina",
"aboveRatio": "Os hóspedes recebem {percent}% da memória da máquina",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"time_synchronisation": {
"title": "Time synchronisation",
"rationale": "NTP e NTPSynchronized tal como o `timedatectl` os reporta. A pertença ao cluster, a validação de certificados e a ordem dos registos dependem de relógios concordantes. Outro mecanismo pode estar a disciplinar o relógio.",
"summary": {
"synchronised": "The clock is synchronised with a time source",
"disabled": "Time synchronisation is disabled",
"notSynchronised": "Time synchronisation is enabled but the clock is not synchronised",
"evaluationFailed": "The check could not be evaluated"
"synchronised": "O relógio está sincronizado com uma fonte de tempo",
"notSynchronised": "A sincronização do tempo está activa mas o relógio não está sincronizado",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"kernel_current": {
@@ -5420,21 +5426,21 @@
"rationale": "O kernel em execução face àquele que o anfitrião arrancaria a seguir, conforme o `proxmox-boot-tool` o reporta. Um kernel mais recente apenas instalado pode estar retido de propósito; uma diferença após reiniciar indica um arranque que não vingou.",
"summary": {
"current": "O kernel em execução {version} é o que o anfitrião arrancaria a seguir",
"newerAvailable": "The host runs {running} while {newest} is installed",
"newerSelected": "O anfitrião executa {running} e arrancaria {selected} no próximo reinício",
"wouldDowngrade": "O anfitrião executa {running} mas arrancaria o mais antigo {selected} no próximo reinício",
"bootTargetUnknown": "O anfitrião executa {version}; não foi possível ler o kernel escolhido para o próximo arranque",
"evaluationFailed": "The check could not be evaluated"
"newerAvailable": "A máquina executa o {running} enquanto o {newest} está instalado",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"security_updates": {
"title": "Security updates",
"rationale": "Pacotes pendentes cuja origem é um repositório de segurança, a partir de um `apt-get upgrade` simulado. O número reflete o que o apt reporta, não a gravidade do que cada pacote corrige.",
"summary": {
"none": "No package updates are pending",
"noSecurity": "{total} update(s) pending, none from a security repository",
"pending": "{count} of {total} pending update(s) come from a security repository",
"evaluationFailed": "The check could not be evaluated"
"none": "Nenhuma atualização do pacote está pendente",
"noSecurity": "Atualização do {total} pendente, nenhuma de um repositório de segurança",
"pending": "O {count} do {total} ainda pendentes é proveniente de um repositório de segurança",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"journal_size": {
@@ -5442,10 +5448,10 @@
"rationale": "O journal em disco face ao limite que se lhe aplica: SystemMaxUse quando está definido e, caso contrário, o valor por omissão do journald, um décimo do sistema de ficheiros onde reside.",
"summary": {
"bounded": "O journal ocupa {size}, dentro do seu limite efetivo",
"large": "The journal holds {size} on disk",
"nearCap": "O journal ocupa {size} e está a {percent}% do seu limite efetivo",
"capUnknown": "O journal ocupa {size}; não foi possível determinar o seu limite efetivo",
"evaluationFailed": "The check could not be evaluated"
"large": "O diário contém {size} no disco",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"swap_configured": {
@@ -5454,7 +5460,7 @@
"summary": {
"active": "{size} of swap is active",
"none": "No swap area is active",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"filesystem_capacity": {
@@ -5500,7 +5506,7 @@
"summary": {
"synchronised": "As {total} partições de arranque levam os mesmos kernels",
"attention": "{count} de {total} partições de arranque requerem revisão",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
},
"rationale": "As partições do sistema EFI proxmox-boot-tool reportam e os kernels que cada uma carrega. proxmox-boot-tool status pode montar temporariamente partições do sistema EFI; nenhuma inicialização é tentada."
},
@@ -5510,7 +5516,7 @@
"summary": {
"allRunning": "Os {total} serviços essenciais estão ativos e nenhuma unidade falhou",
"attention": "{count} constatação(ões) entre as unidades",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"ha_state": {
@@ -5519,7 +5525,7 @@
"summary": {
"managed": "Os {total} serviços geridos estão num estado assente em {nodes} nó(s)",
"attention": "{count} constatação(ões) sobre {total} serviço(s) gerido(s)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
}
},
@@ -5528,27 +5534,27 @@
"title": "Container privileges",
"rationale": "A definição `unprivileged` de cada configuração de contentor. A sua ausência significa que o contentor partilha o espaço de nomes de utilizador do anfitrião, algo que certas cargas necessitam.",
"summary": {
"allUnprivileged": "All {total} containers are unprivileged",
"privileged": "{count} of {total} containers run privileged",
"evaluationFailed": "The check could not be evaluated"
"allUnprivileged": "Todos os recipientes {total} não são privilegiados",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"qemu_without_agent": {
"title": "Guest agent on virtual machines",
"rationale": "A definição `agent` na configuração de cada máquina virtual. A definição indica que o agente está declarado, não que responda.",
"summary": {
"allHaveAgent": "All {total} virtual machines declare the guest agent",
"missingAgent": "{count} of {total} virtual machines do not declare the guest agent",
"evaluationFailed": "The check could not be evaluated"
}
"allHaveAgent": "Todas as máquinas virtuais {total} declaram o agente convidado",
"missingAgent": "{count} das máquinas virtuais {total} não declaram o agente convidado",
"evaluationFailed": "A verificação não pôde ser avaliada"
},
"title": "Agente convidado em máquinas virtuais"
},
"autostart": {
"title": "Automatic start",
"rationale": "A definição `onboot` de cada convidado, excluindo modelos e convidados geridos por HA. Se se espera que um convidado volte por si só, indica-o a política declarada.",
"summary": {
"allAutostart": "All {total} guests start with the host",
"notAutostart": "{count} of {total} guests do not start with the host",
"evaluationFailed": "The check could not be evaluated"
"allAutostart": "Todos os hóspedes do {total} começam com o anfitrião",
"notAutostart": "{count} dos hóspedes do {total} não começam com a máquina",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"stuck_snapshots": {
@@ -5556,28 +5562,28 @@
"rationale": "Estado e antiguidade dos snapshots e tarefas ativas. Uma operação recente ou sem data verificável não é considerada interrompida.",
"summary": {
"noSnapshots": "No guest holds snapshots",
"allComplete": "The {total} snapshot(s) are complete",
"stuck": "{count} of {total} snapshot(s) were left mid-operation",
"evaluationFailed": "The check could not be evaluated"
"allComplete": "Os instantâneos {total} estão completos",
"stuck": "{count} dos instantâneos {total} foram deixados no meio da operação",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"cpu_host_type": {
"title": "Virtual CPU model",
"rationale": "O valor `cpu` de cada máquina virtual. `host` expõe o conjunto de instruções do processador físico, o que limita os nós para onde o convidado pode migrar. A incompatibilidade com um destino concreto não se determina aqui.",
"summary": {
"none": "None of the {total} virtual machines is pinned to the host processor",
"pinned": "{count} of {total} virtual machines are pinned to the host processor",
"evaluationFailed": "The check could not be evaluated"
"none": "Nenhuma das máquinas virtuais {total} está presa ao processador host",
"pinned": "{count} de máquinas virtuais {total} são fixados ao processador host",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"replication_state": {
"title": "Replication",
"rationale": "Tarefas de replicação a partir da API: número de falhas, último erro, última sincronização e o calendário que cada tarefa declara. As tarefas em pausa são assinaladas como tal.",
"summary": {
"healthy": "The {total} replication job(s) report no error",
"failing": "{count} of {total} replication job(s) report an error",
"statusUnavailable": "Replication jobs are defined but their status could not be read",
"evaluationFailed": "The check could not be evaluated"
"healthy": "A tarefa de replicação do {total} não reporta nenhum erro",
"failing": "{count} das tarefas de replicação do {total} reportam um erro",
"statusUnavailable": "As tarefas de replicação estão definidas mas o seu estado não pôde ser lido",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
}
},
@@ -5589,39 +5595,39 @@
"bothEnabled": "A ativação da firewall está configurada ao nível do centro de dados e do nó",
"datacenterOff": "A firewall está desativada ao nível do centro de dados, pelo que as regras do nó não são aplicadas",
"nodeOff": "A firewall está ativada ao nível do centro de dados, mas não neste nó",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"lynis_warnings": {
"title": "Lynis warnings",
"rationale": "Avisos da última auditoria do Lynis, cada um com o seu identificador de teste, e a idade dessa auditoria. Só é executada uma auditoria se o Lynis estiver instalado e não existir relatório completo. As sugestões não são incluídas.",
"summary": {
"none": "The last Lynis audit recorded no warnings",
"found": "The last Lynis audit recorded {count} warning(s)",
"incomplete": "The Lynis report is incomplete",
"evaluationFailed": "The check could not be evaluated",
"noneStale": "A última auditoria do Lynis não registou avisos, e o seu relatório tem {days} dia(s)",
"foundStale": "A última auditoria do Lynis registou {count} aviso(s), e o seu relatório tem {days} dia(s)"
"foundStale": "A última auditoria do Lynis registou {count} aviso(s), e o seu relatório tem {days} dia(s)",
"none": "A última auditoria do Lynis não registrou avisos",
"found": "A última auditoria de Lynis registrou avisos {count}",
"incomplete": "O relatório Lynis está incompleto.",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"certificate_expiry": {
"title": "Certificate validity",
"rationale": "A data de validade do certificado que o pveproxy serve a partir de /etc/pve/local. Um certificado próprio tem precedência sobre o que o Proxmox gera.",
"summary": {
"valid": "The certificate is valid for {days} more day(s)",
"expiring": "The certificate expires in {days} day(s)",
"expired": "The certificate expired {days} day(s) ago",
"evaluationFailed": "The check could not be evaluated"
"valid": "O certificado é válido para {days} mais dias",
"expiring": "O certificado expira em {days} dia(s)",
"expired": "O certificado expirou {days} dias atrás",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"ssh_root_login": {
"title": "SSH root access",
"rationale": "PermitRootLogin na configuração efetiva de `sshd -T`, com os métodos de autenticação com que se combina. O Proxmox é entregue com `yes`, que aceita palavra-passe.",
"summary": {
"password": "Root may sign in over SSH with a password",
"keyOnly": "Root may sign in over SSH with a key only",
"denied": "Root may not sign in over SSH",
"evaluationFailed": "The check could not be evaluated"
"password": "Raiz pode entrar sobre SSH com uma senha",
"keyOnly": "Raiz pode entrar sobre SSH com uma chave apenas",
"denied": "A raiz não pode entrar sobre o SSH",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
}
},
@@ -5630,9 +5636,9 @@
"title": "Volume assignment",
"rationale": "Volumes de convidado no armazenamento local face às referências nas configurações atuais, pendentes e de snapshots. Backups, ISOs e modelos ficam fora da comparação. Um volume sem referência é um candidato a revisão.",
"summary": {
"none": "No orphaned volumes were found",
"found": "Há {count} volumes sem referência nas configurações examinadas",
"evaluationFailed": "The check could not be evaluated"
"none": "Não foram encontrados volumes órfãos",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"zfs_arc_max": {
@@ -5640,21 +5646,21 @@
"rationale": "Valores efetivos de c_min, c_max e tamanho do ARC, o parâmetro de módulo carregado e as definições persistentes em /etc/modprobe.d. Um valor configurado a zero seleciona o valor por omissão do módulo; o ARC é um teto e a memória que ocupa é recuperável.",
"summary": {
"bounded": "O limite do ARC é {percent}% da memória do anfitrião, e a memória que ocupa é recuperável",
"high": "The ARC may use {percent}% of host memory",
"unset": "The ARC has no explicit limit set",
"conflicting": "{count} definições do ARC não coincidem entre si",
"pending": "Uma definição persistente do ARC difere do valor que o módulo em execução transporta",
"evaluationFailed": "The check could not be evaluated"
"high": "O ARC pode usar {percent}% da memória da máquina",
"unset": "O ARC não tem limite explícito definido",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"zfs_scrub_age": {
"title": "ZFS scrub",
"rationale": "O último scrub concluído que o `zpool status` regista em cada pool. Um resilver não é um scrub. Uma pool criada há pouco ainda não teve ocasião de executar um.",
"summary": {
"recent": "The {total} pool(s) were scrubbed within the last 35 days",
"overdue": "{count} of {total} pool(s) have not been scrubbed in 35 days",
"neverScrubbed": "{count} pool(s) record no scrub",
"evaluationFailed": "The check could not be evaluated"
"recent": "As piscinas {total} foram esfregadas nos últimos 35 dias",
"overdue": "{count} da(s) piscina(s) {total} não foram esfregadas em 35 dias",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"thin_pool_overprovisioning": {
@@ -5664,7 +5670,7 @@
"withinRatio": "Os {total} thin pools encontram-se abaixo dos limites de revisão aplicados",
"aboveRatio": "{count} de {total} thin pools distribuem mais capacidade do que possuem",
"pressure": "{pressure} de {total} thin pools estão perto de encher os seus dados ou metadados",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"connected_storage": {
@@ -5673,7 +5679,7 @@
"summary": {
"available": "O PVE indica que os {total} armazenamentos estão disponíveis; os componentes internos remotos e o acesso de escrita não foram testados",
"attention": "{count} de {total} armazenamentos requerem revisão",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"pool_integrity": {
@@ -5691,7 +5697,7 @@
"summary": {
"healthy": "O Ceph reporta HEALTH_OK",
"degraded": "O Ceph reporta {state}, com {count} verificação(ões) nomeada(s)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"array_integrity": {
@@ -5700,7 +5706,7 @@
"summary": {
"intact": "Os {total} arrays e mapas mantêm a sua redundância",
"degraded": "{count} de {total} arrays ou mapas estão sem ela",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
}
},
@@ -5712,7 +5718,7 @@
"withinLife": "As {total} leituras de discos não excedem o limiar indicativo de cinco anos",
"pastLife": "{count} de {total} leituras de discos excedem cinco anos de funcionamento",
"noReadings": "Nenhum disco expõe contadores SMART utilizáveis ({skipped} sem leituras)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"disk_errors": {
@@ -5729,23 +5735,22 @@
"title": "Bond members",
"rationale": "O estado MII de cada membro do bond a partir de /proc/net/bonding e quantas ligações restam. Em active-backup, um membro de reserva figura como ativo e não transporta tráfego.",
"summary": {
"allUp": "All members of the {total} bond(s) are up",
"membersDown": "{count} bond member(s) are not up",
"evaluationFailed": "The check could not be evaluated"
"allUp": "Todos os membros da(s) ligação(ões) {total} estão em alta",
"membersDown": "O( s) membro( s) de ligação do {count} não está( s) em espera",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
},
"bridge_without_ports": {
"title": "Bridge ports",
"rationale": "A configuração de portas de cada bridge. Uma bridge sem porta física serve uma rede interna ou encaminhada.",
"summary": {
"allConnected": "The {total} bridge(s) carry a port",
"isolated": "{count} of {total} bridge(s) carry no port",
"evaluationFailed": "The check could not be evaluated"
"allConnected": "A(s) ponte(s) {total} carrega(m) uma porta",
"evaluationFailed": "A verificação não pôde ser avaliada"
}
}
}
},
"summaryFallback": "The check could not be evaluated",
"acceptRisk": {
"action": "Aceitar risco",
"revoke": "Voltar a ativo",
@@ -5779,17 +5784,15 @@
"OK": "OK"
},
"viewSwitch": {
"ariaLabel": "Switch between assessment and inventory",
"assessment": "Assessment",
"inventory": "Inventory",
"policy": "Política",
"changes": "Alterações"
"changes": "Alterações",
"ariaLabel": "Mudança entre avaliação e inventário"
},
"inventory": {
"loading": "Loading inventory…",
"failed": "The inventory could not be composed.",
"collectedAt": "Composed on {when}",
"unavailable": "Not read in this inventory",
"unresolved": "path not resolved",
"noUplink": "no uplink",
"identity": "Node identity",
@@ -5832,12 +5835,10 @@
"protection": "Backup",
"passthrough": "Passthrough",
"noBackup": "No backup",
"noBackupDetail": "No enabled backup job selects this guest.",
"applications": "Applications",
"versionUnknown": "version not detected",
"passthroughTitle": "PCI passthrough",
"iommuGroup": "IOMMU group {group}",
"sharedGroup": "{count} more device(s) in the same group",
"proxmenux": "ProxMenux optimizations",
"latency": "Latência de rede",
"subscriptionStatus": {
@@ -5848,7 +5849,11 @@
"suspended": "Suspensa",
"new": "Aguarda ativação",
"unknown": "Desconhecido"
}
},
"failed": "O inventário não pôde ser composto.",
"unavailable": "Não lido neste inventário",
"noBackupDetail": "Nenhuma tarefa de backup ativada seleciona este convidado.",
"sharedGroup": "{count} mais dispositivo( s) no mesmo grupo"
},
"profile": {
"label": "Report",
@@ -5871,7 +5876,6 @@
"area": "Area",
"inventoryAnnex": "Inventory annex",
"scopeTitle": "Scope of this report",
"scopeBody": "This report describes the Proxmox VE node named above, as observed from the node itself at the time stated. It does not cover the interior of the guests beyond what they declare, network equipment outside the host, physical infrastructure, or any dependency not visible from this node. Findings marked as not determined were not measured and are not evidence of absence.",
"building": "A compor o relatório…",
"node": "Nó",
"profile": "Perfil",
@@ -6020,11 +6024,12 @@
"structureSubtitle": "Como {node} está construído e configurado",
"postureTitle": "Posture",
"posture": {
"security": "Host exposure and access.",
"backup": "Guest protection and whether it is real.",
"capacity": "Room to grow and the wear on the disks."
"security": "Exposição ao hospedeiro e acesso.",
"backup": "Proteção de hóspedes e se é real.",
"capacity": "Espaço para crescer e o desgaste nos discos."
},
"scopeReadOnly": "A avaliação inspeciona as configurações e a integridade do host. Pode escrever relatórios e logs; verificações de status de inicialização podem montar temporariamente partições do sistema EFI."
"scopeReadOnly": "A avaliação inspeciona as configurações e a integridade do host. Pode escrever relatórios e logs; verificações de status de inicialização podem montar temporariamente partições do sistema EFI.",
"scopeBody": "Este relatório descreve o nó Proxmox VE denominado acima, como observado a partir do próprio nó no momento indicado. Não cobre o interior dos hóspedes além do que eles declaram, equipamento de rede fora do host, infraestrutura física, ou qualquer dependência não visível deste nó. Os achados marcados como não determinados não foram medidos e não são evidência de ausência."
},
"results": "Resultados",
"classifications": {
@@ -6178,15 +6183,19 @@
"notApplicableScope": "Nada no âmbito examinado a que esta verificação se aplique.",
"lynis": {
"title": "Run Lynis",
"bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.",
"bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.",
"withLynis": "Run with Lynis",
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
"cancel": "Cancel",
"bodyNotRun": "Lynis está instalado, mas ainda não foi executado. Executá-lo agora completa a revisão de segurança, mas o processo pode demorar alguns minutos.",
"bodyStale": "O relatório Lynis tem {days} dias. Você pode executá-lo agora para atualizar os dados (demora um pouco mais) ou continuar com o relatório existente."
},
"readOnlyNotice": "A avaliação inspeciona as configurações e a integridade do host. Pode escrever relatórios e logs; verificações de status de inicialização podem montar temporariamente partições do sistema EFI.",
"noActionNeeded": "Nada para fazer. Este cheque encontrou o que espera encontrar.",
"couldNotEvaluate": "Esta verificação não pôde ser avaliada, por isso não relata nada de qualquer forma. As provas registram o que não conseguiu ler."
"couldNotEvaluate": "Esta verificação não pôde ser avaliada, por isso não relata nada de qualquer forma. As provas registram o que não conseguiu ler.",
"neverRun": "Este hospedeiro ainda não foi avaliado.",
"noFindings": "Nenhuma descoberta corresponde ao filtro actual.",
"acceptedNotice": "O {count} aceitou o(s) risco(s) registado(s) nesta máquina.",
"summaryFallback": "A verificação não pôde ser avaliada"
},
"runtime": {
"notifications": {
+6 -6
View File
@@ -1508,7 +1508,6 @@
"containerNameLabel": "Názov kontajnera",
"containerNamePlaceholder": "napr. <your-container-name>",
"ociLabelKeyLabel": "Kľúč OCI labelu",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Cesta k Python interpreteru",
"pythonInterpreterPlaceholder": "napr. /opt/<your-app>/venv/bin/python",
"pipDistLabel": "Názov distribúcie (pip balík)",
@@ -1652,7 +1651,8 @@
"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.",
"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čí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.",
"ociLabelPlaceholder": "org.opencontainers.image.version"
}
},
"settings": {
@@ -4936,23 +4936,23 @@
"Zobraziť všetky LXC kontajnery",
"Spustiť LXC kontajner",
"Zastaviť LXC kontajner",
"Otvoriť shell v LXC kontajneri",
"Otvoriť konzolu LXC kontajnera",
"Zobraziť nastavenia kontajnera",
"Zobraziť stav úložísk",
"Zobraziť obsah úložiska",
"Otestovať výkon Proxmox systému",
"Zobraziť verziu Proxmox VE",
"Zobraziť stav služby pve-cluster",
"Skontrolovať stav klastra",
"Zobraziť stav klastra",
"Zobraziť uzly klastra",
"Zobraziť stav ZFS poolu",
"Zobraziť všetky ZFS pooly",
"Zobraziť ZFS súborové systémy a zväzky",
"Zobraziť všetky ZFS datasety",
"Zobraziť podrobnosti o súboroch",
"Prejsť do iného priečinka",
"Vytvoriť priečinok",
"Odstrániť priečinok aj s obsahom",
"Kopírovať súbory",
"Kopírovať súbory alebo priečinky",
"Presunúť alebo premenovať súbory",
"Zobraziť obsah súboru",
"Hľadať text v súbore",
+148 -139
View File
@@ -458,11 +458,11 @@
"extendedNvmeHealth": "Utökad NVMe Health",
"ssdWearLifetime": "SSD slitage och livstid",
"nvmeHealthMetrics": "NVMe Health Metrics",
"sasHealthMetrics": "SAS/SCSI Health Metrics",
"smartAttributes": "SMART-attribut",
"selfTestResult": "Självtestresultat",
"lastSelfTestResult": "Senaste självtestresultat",
"recommendations": "Rekommendationer"
"recommendations": "Rekommendationer",
"sasHealthMetrics": "SAS / SCSI Health Metrics"
},
"statusValues": {
"ok": "OK",
@@ -1487,7 +1487,6 @@
"containerNameLabel": "Behållarens namn",
"containerNamePlaceholder": "t.ex. <ditt-behållarnamn>",
"ociLabelKeyLabel": "OCI-etikettnyckel",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"pythonInterpreterLabel": "Python-tolkväg",
"pythonInterpreterPlaceholder": "t.ex. /opt/<din-app>/venv/bin/python",
"pipDistLabel": "Distributionsnamn (pip-paket)",
@@ -1534,11 +1533,8 @@
"portHttps": "https",
"portLogoLabel": "Logotyp URL för denna länk (valfritt)",
"portLogoPlaceholder": "t.ex. https://example.com/logo.webp",
"portCategoryPlaceholder": "Category for the Apps dashboard (optional)",
"portCategoryNone": "No category",
"portCategoryAddNew": "+ Add new category…",
"portCategoryCustomPlaceholder": "Type a category and press Enter (Esc to cancel)",
"portCustomUrlPlaceholder": "Custom URL (e.g. https://vault.example.com) — overrides IP:port",
"removePortTooltip": "Ta bort porten",
"detectMethodDpkg": "dpkg ·",
"detectMethodApk": "apk ·",
@@ -1631,7 +1627,11 @@
"notifyUpstreamLabel": "Meddela mig när en ny uppströmsversion är tillgänglig",
"notifyUpstreamHelp": "Skickar `app_update_available` till de kanaler som är aktiverade i Inställningar → Aviseringar. Stäng av om den här appen inte kan uppdateras på din box.",
"excludeFromBadgeLabel": "Uteslut från LXC-uppdateringsräknaren",
"excludeFromBadgeHelp": "Räkna inte den här appen i det samlade uppdateringsmärket på LXC-listkortet. Användbart när du är fäst till en specifik version med avsikt (spårningskrav, frysning av kompatibilitet).Påverkar inte appflikens eget tillstånd eller det utgående meddelandet."
"excludeFromBadgeHelp": "Räkna inte den här appen i det samlade uppdateringsmärket på LXC-listkortet. Användbart när du är fäst till en specifik version med avsikt (spårningskrav, frysning av kompatibilitet).Påverkar inte appflikens eget tillstånd eller det utgående meddelandet.",
"ociLabelPlaceholder": "org.opencontainers.image.version",
"portCategoryPlaceholder": "Kategori för Apps dashboard (tillval)",
"portCategoryCustomPlaceholder": "Skriv en kategori och tryck på Enter (Esc för att avbryta)",
"portCustomUrlPlaceholder": "Anpassad URL (t.ex. https://vault.example.com) - åsidosätter IP:port"
},
"statusFilter": {
"ariaLabel": "Filtrera virtuella maskiner och containrar",
@@ -2515,10 +2515,10 @@
"scope": {
"label": "Token permissions",
"readOnly": "Read-only",
"readOnlyHint": "Reads metrics and status. Recommended for dashboards and integrations.",
"fullAdmin": "Full admin",
"fullAdminHint": "Full control, like your own session.",
"fullAdminWarning": "A full-admin token can do everything you can: power off or reboot the host, run updates and open a terminal. Share it only with fully trusted integrations."
"readOnlyHint": "Läser mätvärden och status. Rekommenderas för instrumentbrädor och integrationer.",
"fullAdminHint": "Full kontroll, som din egen session.",
"fullAdminWarning": "En full-admin-token kan göra allt du kan: Strö av eller starta om värden, kör uppdateringar och öppna en terminal. Dela det bara med fullt betrodda integrationer."
}
},
"firewall": {
@@ -3302,26 +3302,33 @@
"gotIt": "Jag förstår!",
"dontShowAgain": "Visa inte igen för den här versionen",
"currentFeatures": {
"appDetection": "Smarter app detection in the App tab: Docker is correctly promoted as the parent workload during cold start (Portainer/SearXNG no longer briefly show up as native apps), unregistered suggestions live in the startup cache, and 'Find applications' runs a fresh catalog-backed scan on demand.",
"dockerUpdates": "The Updates tab now covers Docker end-to-end: Docker Engine and per-image update tracking follow the same 24-hour rolling cycle as OS packages, with a 'Check now' action for on-demand digest comparison — no more waiting for the daily collector.",
"appCatalog": "Ny katalog för appdetektering med över 380 spårade arbetsuppgifter, genererad live från community-scripts via sju detektionsmetoder (fil, binär, dpkg, apk, Python, Docker exec, Docker label). Primära och reservdetektorer täcker både nya och historiska LXC-layouter.",
"pushover": "Pushover joins Telegram, Gotify, Discord, Email and Apprise as a native notification channel — user/API key, device and sound selectors, priority 0 for regular messages, optional priority 1 for CRITICAL events. Suggested by @benginx (#308).",
"appsDashboard": "Ny huvudflik Apps — en enda startpunkt för varje webblänk på noden. LXC-registrerade appar och användardefinierade Custom Web Links delar samma rutnät med kategori-taggar, sökning och direktlänk till gästens modal.",
"lxcAppsUpdates": "App-fliken inuti varje LXC-modal registrerar installerade appar, fångar webblänkar och spårar uppströmsversioner. Omarbetad Updates-flik applicerar OS-paket och appuppdateringar med en enda knapp; Docker Engine och per-image följer samma 24-timmarscykel med en 'Kontrollera nu'-åtgärd på begäran.",
"multilingual": "Monitorn talar nu 8 språk: engelska, spanska, tyska, franska, italienska, portugisiska, svenska och slovakiska. Ett stort tack till @vaso73 för att ha byggt i18n-grunden.",
"nvidiaMultiGpu": "NVIDIA-driverns livscykel går över till ägarskap per exakt BDF, så att en multi-GPU-värd kan skicka ett kort till en VM och behålla det andra operativt på värden eller i LXC, plus en versionsväljare som är medveten om kärna, gren och GPU (#298).",
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
"auditAssessment": "Audit & Report — a new page that documents and assesses the node. The Assessment runs a catalogue of checks across security, backups, capacity, storage, network, hardware, guests and system, classifying each finding as critical, warning, observation or conformant and stating what it read rather than judging it.",
"changeJournal": "Change journal — a Changes view that lists exactly what ProxMenux modified on this host: every configuration file, package and service it touched, with the prior state of each, deduplicated to a current-state view that shows the host as it stands now rather than a log of every run.",
"auditReports": "Reports — a full audit plus focused Security review, Backup assurance and Capacity & wear reports, each opening on its own posture header, and a printable Inventory; all export to PDF. A security assessment asks before running Lynis so a run stays fast.",
"auditPolicyBaseline": "Policy and baseline — declare what the host is expected to be (backup requirements, firewall, root SSH login) so findings grade against it, mark a run as the reference, and see what changed since, with new, resolved and accepted findings kept apart.",
"groupedAppUpdates": "Grouped application update notifications — one complete message per scan instead of one per application, grouped by LXC with the installed and available versions.",
"adminTokenScope": "Administrative scope — opening a Monitor terminal and disabling authentication now require a full-admin token, so a read-only API token issued to a monitoring integration stays read-only (reported by @f3rs3n)."
"appDetection": "Smartare app detektion i fliken App: Docker är korrekt främjas som moder arbetsbelastning under kallstart (Portainer / SearXNG inte längre kort visas som inhemska appar), oregistrerade förslag lever i uppstartscache, och \"Find applikationer\" kör en ny katalogstödd skanning på begäran.",
"dockerUpdates": "fliken Uppdateringar täcker nu Docker end-to-end: Docker Motor- och per-image-uppdateringsspårning följer samma 24-timmars rullcykel som OS-paket, med en \"Check now\" -åtgärd för on-demand digest jämförelse - inte mer väntar på den dagliga samlaren.",
"pushover": "Pushover ansluter sig till Telegram, Gotify, Discord, Email and Apprise som en infödd meddelandekanal - användar / API-nyckel, enhet och ljudväljare, prioritet 0 för vanliga meddelanden, valfri prioritet 1 för CRITICAL-evenemang. Föreslås av @benginx (#308).",
"aiCustomEndpoint": "AI Assistant anpassade OpenAI endpoint - LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute och någon själv värd proxy på privata IPs, loopback eller Docker-nätverk är erkända när man laddar modellkatalogen. Avstängningen ytor serverns fel (eller den underliggande nätverksskäl) direkt under Load-knappen (#325, rapporterad av @jorgeffonte).",
"secureGatewayArch": "Säker Gateway guide - Alpine mall nedladdning, lokal mallval och pct create alla matcha värdens verkliga arkitektur, så x86 64 värdar får amd64 behållare och arm64 värdar får arm64 behållare (#324, rapporterad av @N0X4D0).",
"atomicNotifications": "Anmälningshändelser förbehåller sig sin deduplicering fingeravtryck atomiskt innan AI-behandling och kanalleverans, så samtidiga samlare, slutförande återkopplingar eller parallella övervakningsprocesser kan inte skicka samma händelse två gånger. Bokningen släpps när ingen kanal lyckas, bevara retries.",
"borgSshPort": "Borg fjärrmål - Add Borg destination dialog och skalet TUI acceptera en anpassad SSH hamn. BORG RSH, auto key install flow och kapacitet sond alla hedra det. Fullt bakåtkompatibel med befintliga poster som skapats utan en explicit port (förslag av @songochain i diskussion #236).",
"githubToken": "Inställningar → GitHub API accepterar en valfri personlig åtkomsttoken för release och tag kontroller när den anonyma kvoten är utmattad. Token krypteras i vila och återvände aldrig till webbläsaren; hastighetsfel översätts i varje Monitor-språk (föreslås av @SystemIdleProcess i diskussion #306).",
"replicationContext": "Native Proxmox replikationsfelmeddelanden löser replikationsjobbet ID, påverkade VM / LXC ID och gästnamn; det exakta felblocket från Proxmox bevaras som orsaken, och varje replikationsjobb dedupliceras oberoende (rapporteras av Ale R.).",
"auditAssessment": "Audit & Report – en ny sida som dokumenterar och bedömer noden. Bedömningen driver en katalog över kontroller över säkerhet, säkerhetskopior, kapacitet, lagring, nätverk, hårdvara, gäster och system, klassificera varje hitta som kritisk, varning, observation eller överensstämmelse och ange vad det läser snarare än att döma det.",
"changeJournal": "Ändra tidskrift - en Ändringsvy som listar exakt vad ProxMenux modifierade på denna värd: varje konfigurationsfil, paket och tjänst som den rörde, med föregående tillstånd av varje, deduplicerad till en aktuell statsvy som visar värden som det står nu snarare än en logg på varje körning.",
"auditReports": "Rapporter - en fullständig revision plus fokuserad säkerhetsgranskning, säkerhetskopiering och kapacitet & slitage rapporter, varje öppning på sin egen hållning rubrik, och en utskrivbar lager; all export till PDF. En säkerhetsbedömning frågar innan du kör Lynis så en körning stannar snabbt.",
"auditPolicyBaseline": "Policy och baslinje - förklara vad värden förväntas vara (backup krav, brandvägg, rot SSH inloggning) så resultat betyg mot det, markera en körning som referens, och se vad som förändrats sedan, med nya, lösta och accepterade fynd hålls isär.",
"groupedAppUpdates": "Grupperade programuppdateringsmeddelanden - ett komplett meddelande per skanning istället för en per applikation, grupperad av LXC med installerade och tillgängliga versioner.",
"adminTokenScope": "Administrativa räckvidd - öppna en Monitor-terminal och inaktivera autentisering kräver nu en full-admin-token, så en lättläst API-token utfärdad till en övervakningsintegration förblir lättläst (rapporterad av @f3rs3n).",
"ociAppTab": "OCI-containrar i fliken App — en container som installerats av OCI manager Apps känns igen från sin installationspost: applikationen och dess avbild, en ny avbild som upptäcks via digest och en länk till avbildens repository.",
"ociUpdatesTab": "Uppdateringar för OCI-containrar — Uppdatera och Återskapa öppnar samma flöde som OCI-menyn, säkerhetskopian som tas före uppdateringen kan behållas i en lagring för säkerhetskopior och avbilden kan uppdateras enligt schema.",
"ociLogsTab": "Fliken Logs för OCI-containrar — applikationens konsolutdata, sparad på värden och följd i realtid, med filter och nedladdning. Proxmox-konsolen för dessa containrar öppnar ett skal.",
"appsUpdateShortcut": "Uppdateringsikonen på sidan Appar öppnar containern direkt på dess flik Uppdateringar.",
"webhookHttps": "Proxmox-aviseringar når Monitor med HTTPS aktiverat — de levereras till en adress som bara är lokal och misslyckas inte längre på grund av ett certifikatfel.",
"persistentLogs": "En avslutad skur av loggfel rapporteras inte längre som bestående: ett mönster måste fortsätta att dyka upp i 15 minuter, och dess varning stängs av sig själv (rapporterat av @Joshua1264).",
"mountsLanAddress": "Monteringspunkter på LVM-thin och annan blocklagring visar sin användning, och program med flera containrar öppnas på sin LAN-adress."
}
},
"network": {
@@ -3546,7 +3553,6 @@
"latencyHistory": "Latenshistorik (senaste {count} poster)",
"methodology": "Metodik",
"testMethod": "Testmetod",
"icmpEchoRequest": "ICMP Echo Request (Ping)",
"samplesPerTest": "Prover per test",
"threeConsecutivePings": "3 plingar i följd",
"targetIp": "Mål-IP",
@@ -3558,7 +3564,8 @@
"assessmentUnknown": "Det går inte att fastställa nätverksstatus.",
"footerTitle": "ProxMenux Monitor - Nätverksprestandarapport",
"generated": "Genererad",
"reportId": "Rapport-ID"
"reportId": "Rapport-ID",
"icmpEchoRequest": "ICMP Echo Request (Ping)"
}
}
},
@@ -5027,23 +5034,20 @@
"uncategorized": "Uncategorized",
"openAriaLabel": "Open {name} in a new tab",
"openGuestAriaLabel": "Öppna {name} ({type} {id})",
"emptyTitle": "No apps with a web link yet.",
"emptyHint": "Register a Web Link for any app (App tab of a CT) to see it here.",
"customLinkAdd": "Add link",
"editModeToggle": "Edit",
"editModeDone": "Done",
"customLinkNewTitle": "New web link",
"customLinkEditTitle": "Edit web link",
"customLinkEditAria": "Redigera den anpassade länken {name}",
"openUpdatesAria": "Öppna fliken Uppdateringar för {name}",
"openUpdatesTitle": "Uppdatering tillgänglig — öppna fliken Uppdateringar",
"customLinkName": "Name",
"customLinkNamePlaceholder": "e.g. My app",
"customLinkUrl": "URL",
"customLinkLogo": "Logo URL (optional)",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkCategory": "Category (optional)",
"customLinkBinding": "Bound to",
"customLinkBindingNone": "Not bound to any guest",
"customLinkBindingHelp": "Bind this link to a VM or CT to add its ID + name to the card and jump to the guest with one click.",
"customLinkSave": "Save",
"customLinkCreate": "Create",
"customLinkCancel": "Cancel",
@@ -5079,7 +5083,12 @@
"remoteAccessVpn": "Remote Access & VPN",
"webserversProxies": "Webservers & Proxies",
"zigbeeZwaveMatter": "ZigBee, Z-Wave & Matter"
}
},
"emptyTitle": "Inga appar med en webblänk ännu.",
"emptyHint": "Registrera en webblänk för någon app (App-fliken på en CT) för att se den här.",
"customLinkLogoPlaceholder": "e.g., https://example.com/logo.webp",
"customLinkBindingNone": "Inte bunden till någon gäst",
"customLinkBindingHelp": "Bind denna länk till en VM eller CT för att lägga till sitt ID +-namn till kortet och hoppa till gästen med ett klick."
},
"audit": {
"presentation": {
@@ -5234,13 +5243,10 @@
"loading": "Loading assessment…",
"run": "Run assessment",
"running": "Assessing…",
"neverRun": "This host has not been assessed yet.",
"lastRun": "Last assessed on {when}",
"stale": "{days} days ago",
"unverifiedChecks": "Ej gjorda: {checks}. Var och en anger i sina belägg vad den inte kunde läsa.",
"noFindings": "No findings match the current filter.",
"affectedCount": "{count} affected",
"acceptedNotice": "{count} accepted risk(s) recorded on this host.",
"reviewOn": "Granska den: {when}",
"reviewDue": "Dags att granska — beslutet gäller fortfarande",
"reviewDueNotice": "{count} accepterade beslut väntar på granskning.",
@@ -5272,7 +5278,7 @@
"sources": "Källor och insamlingstider"
},
"errors": {
"runFailed": "The assessment could not be started."
"runFailed": "Bedömningen kunde inte påbörjas."
},
"checks": {
"backup": {
@@ -5285,7 +5291,7 @@
"uncovered": "{count} av {total} gäster väljs inte av något aktiverat säkerhetskopieringsjobb",
"excludedData": "Granska {count} undantag för diskar eller monteringspunkter",
"uncoveredExpected": "{required} gäster som deklarerats behöva säkerhetskopia väljs inte av något aktiverat jobb",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
},
"nextStep": {
"noJobs": "Skapa ett backup jobb på denna nod och välj de gäster som håller data som du inte vill bygga för hand. Ett jobb som existerar men är funktionshindrat väljer ingenting.",
@@ -5300,10 +5306,10 @@
"rationale": "Ålder: tid som gått sedan den senaste lagrade säkerhetskopian. Använd gräns: referensåldern som kopian jämförs med.",
"summary": {
"recent": "Alla {total} gäst-/destinationskontroller uppfyller det angivna ålderskravet",
"stale": "{count} of {total} guests have no backup from the last 30 days",
"noBackups": "No stored backup matches a guest on this node",
"attention": "{count} av {total} gäst-/destinationskontroller behöver granskas",
"evaluationFailed": "The check could not be evaluated"
"stale": "{count} av {total} gäster har ingen backup från de senaste 30 dagarna",
"noBackups": "Ingen lagrad backup matchar en gäst på denna nod",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
},
"nextStep": {
"stale": "Ta reda på varför jobbet slutade producera kopior för dessa gäster: det kan ha varit inaktiverat, dess schema kan aldrig elda, eller dess körningar kan misslyckas. Löpresultaten kontrollerar hur varje jobb senast slutade.",
@@ -5317,10 +5323,10 @@
"rationale": "Lagringstiden som Proxmox löser upp den: jobbets inställning, sedan lagringens, sedan nodens standardvärde. Lagringstid som en säkerhetskopieringsserver tillämpar går inte att läsa från den här noden.",
"summary": {
"allDefined": "Alla {total} jobb löser upp en inställning för lagringstid",
"missing": "{count} of {total} enabled job(s) declare no retention",
"notDeclared": "{count} av {total} jobb behåller varje kopia: ingen lagringstid är deklarerad",
"onServer": "{count} av {total} jobb skriver till en säkerhetskopieringsserver, som gallrar dem med egna jobb",
"evaluationFailed": "The check could not be evaluated"
"missing": "{count} av {total} aktiverade jobb (er) förklarar ingen lagring",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
},
"nextStep": {
"missing": "Ange en lagring på dessa jobb, eller på lagringen de skriver till. Proxmox tar jobbets inställning först, sedan lagringen, sedan noden standard.",
@@ -5383,36 +5389,36 @@
"summary": {
"none": "Ingenting har begärt omstart",
"pending": "{count} poster är installerade och väntar på omstart",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"enterprise_repo_without_subscription": {
"title": "Enterprise repository",
"rationale": "Referenser till `enterprise.proxmox.com` i `/etc/apt/sources.list` och `sources.list.d`, mot statusen från `pvesubscription get`.",
"summary": {
"notEnabled": "The enterprise repository is not enabled",
"subscribed": "The enterprise repository is backed by a subscription",
"unsubscribed": "The enterprise repository is enabled without an active subscription",
"evaluationFailed": "The check could not be evaluated"
"notEnabled": "Företagsförvaret är inte aktiverat",
"subscribed": "Företagsregister stöds av en prenumeration",
"unsubscribed": "Företagsregister är aktiverat utan aktiv prenumeration",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"memory_overcommit": {
"title": "Memory allocation",
"rationale": "Taket `memory` i varje gästkonfiguration mot MemTotal, med körande gäster räknade för sig. Containrar förbrukar upp till den gränsen; virtuella maskiner utan ballooning reserverar den.",
"summary": {
"withinRatio": "Guests are allocated {percent}% of host memory",
"aboveRatio": "Guests are allocated {percent}% of host memory",
"evaluationFailed": "The check could not be evaluated"
"withinRatio": "Gäster tilldelas {percent}% av värdminnet",
"aboveRatio": "Gäster tilldelas {percent}% av värdminnet",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"time_synchronisation": {
"title": "Time synchronisation",
"rationale": "NTP och NTPSynchronized som `timedatectl` rapporterar dem. Klustermedlemskap, certifikatvalidering och loggordning bygger på klockor som stämmer överens. En annan mekanism kan sköta klockan.",
"summary": {
"synchronised": "The clock is synchronised with a time source",
"disabled": "Time synchronisation is disabled",
"notSynchronised": "Time synchronisation is enabled but the clock is not synchronised",
"evaluationFailed": "The check could not be evaluated"
"synchronised": "Klockan synkroniseras med en tidskälla",
"notSynchronised": "Tidssynkronisering är aktiverad men klockan synkroniseras inte",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"kernel_current": {
@@ -5420,21 +5426,21 @@
"rationale": "Kärnan som körs mot den värden skulle starta härnäst, som `proxmox-boot-tool` rapporterar den. En nyare kärna som bara är installerad kan hållas tillbaka med avsikt; en skillnad efter omstart betyder en start som inte tog.",
"summary": {
"current": "Kärnan {version} som körs är den värden skulle starta härnäst",
"newerAvailable": "The host runs {running} while {newest} is installed",
"newerSelected": "Värden kör {running} och skulle starta {selected} vid nästa omstart",
"wouldDowngrade": "Värden kör {running} men skulle starta den äldre {selected} vid nästa omstart",
"bootTargetUnknown": "Värden kör {version}; kärnan som valts för nästa start kunde inte läsas",
"evaluationFailed": "The check could not be evaluated"
"newerAvailable": "Värden kör {running} medan {newest} är installerad",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"security_updates": {
"title": "Security updates",
"rationale": "Väntande paket vars ursprung är ett säkerhetsarkiv, från en simulerad `apt-get upgrade`. Antalet speglar vad apt rapporterar, inte hur allvarligt det varje paket rättar är.",
"summary": {
"none": "No package updates are pending",
"noSecurity": "{total} update(s) pending, none from a security repository",
"pending": "{count} of {total} pending update(s) come from a security repository",
"evaluationFailed": "The check could not be evaluated"
"none": "Inga paketuppdateringar väntar",
"noSecurity": "{total} uppdatering (er) i väntan, ingen från ett säkerhetsförvar",
"pending": "{count} av {total} pågående uppdateringar kommer från ett säkerhetsförvar",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"journal_size": {
@@ -5442,10 +5448,10 @@
"rationale": "Journalen på disk mot det tak som gäller för den: SystemMaxUse där det är satt, annars journalds standard på en tiondel av filsystemet den ligger på.",
"summary": {
"bounded": "Journalen upptar {size}, inom sitt gällande tak",
"large": "The journal holds {size} on disk",
"nearCap": "Journalen upptar {size} och ligger på {percent}% av sitt gällande tak",
"capUnknown": "Journalen upptar {size}; dess gällande tak kunde inte fastställas",
"evaluationFailed": "The check could not be evaluated"
"large": "Tidskriften håller {size} på disk",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"swap_configured": {
@@ -5454,7 +5460,7 @@
"summary": {
"active": "{size} of swap is active",
"none": "No swap area is active",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"filesystem_capacity": {
@@ -5500,7 +5506,7 @@
"summary": {
"synchronised": "De {total} startpartitionerna bär samma kärnor",
"attention": "{count} av {total} startpartitioner behöver ses över",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
},
"rationale": "EFI-systempartitionerna proxmox-boot-tool rapporter och de kärnor som var och en bär. proxmox-boot-tool status kan tillfälligt montera EFI-systempartitioner; ingen start görs."
},
@@ -5510,7 +5516,7 @@
"summary": {
"allRunning": "De {total} väsentliga tjänsterna är aktiva och ingen enhet har misslyckats",
"attention": "{count} iakttagelse(r) bland enheterna",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"ha_state": {
@@ -5519,7 +5525,7 @@
"summary": {
"managed": "De {total} hanterade tjänsterna är i ett stabilt läge på {nodes} nod(er)",
"attention": "{count} iakttagelse(r) för {total} hanterade tjänster",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
}
},
@@ -5528,27 +5534,27 @@
"title": "Container privileges",
"rationale": "Inställningen `unprivileged` i varje containerkonfiguration. Att den saknas betyder att containern delar värdens användarnamnrymd, vilket vissa laster kräver.",
"summary": {
"allUnprivileged": "All {total} containers are unprivileged",
"privileged": "{count} of {total} containers run privileged",
"evaluationFailed": "The check could not be evaluated"
"allUnprivileged": "Alla {total} behållare är oprivilegierade",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"qemu_without_agent": {
"title": "Guest agent on virtual machines",
"rationale": "Inställningen `agent` i varje virtuell maskins konfiguration. Inställningen säger att agenten är deklarerad, inte att den svarar.",
"summary": {
"allHaveAgent": "All {total} virtual machines declare the guest agent",
"missingAgent": "{count} of {total} virtual machines do not declare the guest agent",
"evaluationFailed": "The check could not be evaluated"
}
"allHaveAgent": "Alla {total} virtuella maskiner förklarar gästagenten",
"missingAgent": "{count} av {total} virtuella maskiner förklarar inte gästagenten",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
},
"title": "Gästagent på virtuella maskiner"
},
"autostart": {
"title": "Automatic start",
"rationale": "Inställningen `onboot` för varje gäst, utom mallar och gäster som HA hanterar. Om en gäst förväntas komma tillbaka av sig själv framgår av den deklarerade policyn.",
"summary": {
"allAutostart": "All {total} guests start with the host",
"notAutostart": "{count} of {total} guests do not start with the host",
"evaluationFailed": "The check could not be evaluated"
"allAutostart": "Alla {total} gäster börjar med värden",
"notAutostart": "{count} av {total} gäster börjar inte med värden",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"stuck_snapshots": {
@@ -5556,28 +5562,28 @@
"rationale": "Ögonblicksbildernas tillstånd och ålder samt aktiva uppgifter. En färsk åtgärd eller en utan verifierbart datum betraktas inte som avbruten.",
"summary": {
"noSnapshots": "No guest holds snapshots",
"allComplete": "The {total} snapshot(s) are complete",
"stuck": "{count} of {total} snapshot(s) were left mid-operation",
"evaluationFailed": "The check could not be evaluated"
"allComplete": "{total} snapshot(s) är kompletta",
"stuck": "{count} av {total} snapshot(s) lämnades mitten av driften",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"cpu_host_type": {
"title": "Virtual CPU model",
"rationale": "Värdet `cpu` för varje virtuell maskin. `host` exponerar den fysiska processorns instruktionsuppsättning, vilket begränsar vilka noder gästen kan migrera till. Oförenlighet med ett visst mål avgörs inte här.",
"summary": {
"none": "None of the {total} virtual machines is pinned to the host processor",
"pinned": "{count} of {total} virtual machines are pinned to the host processor",
"evaluationFailed": "The check could not be evaluated"
"none": "Ingen av {total} virtuella maskiner är fäst vid värdprocessorn",
"pinned": "{count} av {total} virtuella maskiner är fästa på värdprocessorn",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"replication_state": {
"title": "Replication",
"rationale": "Replikeringsjobb från API:et: antal fel, senaste fel, senaste synkronisering och den kalender varje jobb anger. Pausade jobb redovisas som sådana.",
"summary": {
"healthy": "The {total} replication job(s) report no error",
"failing": "{count} of {total} replication job(s) report an error",
"statusUnavailable": "Replication jobs are defined but their status could not be read",
"evaluationFailed": "The check could not be evaluated"
"healthy": "{total} replikeringsjobb (er) rapporterar inga fel",
"failing": "{count} av {total} replikeringsjobb (er) rapporterar ett fel",
"statusUnavailable": "Replikationsjobb definieras men deras status kan inte läsas",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
}
},
@@ -5589,39 +5595,39 @@
"bothEnabled": "Brandväggsaktivering är konfigurerad på datacenter- och nodnivå",
"datacenterOff": "Brandväggen är avstängd på datacenternivå, så nodreglerna tillämpas inte",
"nodeOff": "Brandväggen är aktiverad på datacenternivå men inte på den här noden",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"lynis_warnings": {
"title": "Lynis warnings",
"rationale": "Varningar från den senaste Lynis-granskningen, var och en med sin testidentifierare, och hur gammal granskningen är. En granskning körs bara om Lynis är installerat och ingen fullständig rapport finns. Förslag ingår inte.",
"summary": {
"none": "The last Lynis audit recorded no warnings",
"found": "The last Lynis audit recorded {count} warning(s)",
"incomplete": "The Lynis report is incomplete",
"evaluationFailed": "The check could not be evaluated",
"noneStale": "Den senaste Lynis-granskningen registrerade inga varningar, och dess rapport är {days} dag(ar) gammal",
"foundStale": "Den senaste Lynis-granskningen registrerade {count} varning(ar), och dess rapport är {days} dag(ar) gammal"
"foundStale": "Den senaste Lynis-granskningen registrerade {count} varning(ar), och dess rapport är {days} dag(ar) gammal",
"none": "Den sista Lynis-revisionen registrerade inga varningar",
"found": "Den sista Lynis-revisionen spelade in {count}-varning (er)",
"incomplete": "Lynis-rapporten är ofullständig",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"certificate_expiry": {
"title": "Certificate validity",
"rationale": "Utgångsdatum för certifikatet som pveproxy levererar från /etc/pve/local. Ett eget certifikat går före det Proxmox genererar.",
"summary": {
"valid": "The certificate is valid for {days} more day(s)",
"expiring": "The certificate expires in {days} day(s)",
"expired": "The certificate expired {days} day(s) ago",
"evaluationFailed": "The check could not be evaluated"
"valid": "Intyget gäller för {days} fler dagar (er)",
"expiring": "Certifikatet löper ut i {days}-dagar",
"expired": "Certifikatet löpte ut {days}-dagar sedan",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"ssh_root_login": {
"title": "SSH root access",
"rationale": "PermitRootLogin i den effektiva `sshd -T`-konfigurationen, med de autentiseringsmetoder den kombineras med. Proxmox levereras med `yes`, som accepterar lösenord.",
"summary": {
"password": "Root may sign in over SSH with a password",
"keyOnly": "Root may sign in over SSH with a key only",
"denied": "Root may not sign in over SSH",
"evaluationFailed": "The check could not be evaluated"
"password": "Root kan logga in över SSH med ett lösenord",
"keyOnly": "Rot kan logga in över SSH med en nyckel endast",
"denied": "Rot får inte logga in över SSH",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
}
},
@@ -5630,9 +5636,9 @@
"title": "Volume assignment",
"rationale": "Gästvolymer på lokal lagring mot referenserna i aktuella, väntande och ögonblicksbildskonfigurationer. Säkerhetskopior, ISO-filer och mallar ligger utanför jämförelsen. En volym utan referens är en kandidat för granskning.",
"summary": {
"none": "No orphaned volumes were found",
"found": "{count} volymer saknar referens i de granskade konfigurationerna",
"evaluationFailed": "The check could not be evaluated"
"none": "Inga föräldralösa volymer hittades",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"zfs_arc_max": {
@@ -5640,21 +5646,21 @@
"rationale": "Effektiva värden för c_min, c_max och ARC-storlek, den laddade modulparametern och de bestående inställningarna i /etc/modprobe.d. Ett konfigurerat nollvärde väljer modulens standard; ARC är ett tak och minnet under det är återvinningsbart.",
"summary": {
"bounded": "ARC-gränsen är {percent}% av värdens minne, och minnet under den är återvinningsbart",
"high": "The ARC may use {percent}% of host memory",
"unset": "The ARC has no explicit limit set",
"conflicting": "{count} ARC-inställningar stämmer inte överens",
"pending": "En bestående ARC-inställning skiljer sig från värdet den körande modulen bär",
"evaluationFailed": "The check could not be evaluated"
"high": "ARC kan använda {percent}% av värdminnet",
"unset": "ARC har ingen tydlig gränsuppsättning",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"zfs_scrub_age": {
"title": "ZFS scrub",
"rationale": "Den senaste avslutade scrub som `zpool status` registrerar för varje pool. En resilver är inte en scrub. En nyligen skapad pool har ännu inte haft tillfälle till en.",
"summary": {
"recent": "The {total} pool(s) were scrubbed within the last 35 days",
"overdue": "{count} of {total} pool(s) have not been scrubbed in 35 days",
"neverScrubbed": "{count} pool(s) record no scrub",
"evaluationFailed": "The check could not be evaluated"
"recent": "{total} pool(s) skrubbades under de senaste 35 dagarna",
"overdue": "{count} av {total} pool(s) har inte skrubbats på 35 dagar",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"thin_pool_overprovisioning": {
@@ -5664,7 +5670,7 @@
"withinRatio": "De {total} thin-poolerna ligger under de tillämpade granskningsgränserna",
"aboveRatio": "{count} av {total} thin-pooler delar ut mer kapacitet än de har",
"pressure": "{pressure} av {total} thin-pooler närmar sig att fylla data eller metadata",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"connected_storage": {
@@ -5673,7 +5679,7 @@
"summary": {
"available": "PVE rapporterar alla {total} lagringar som tillgängliga; fjärrsystemens interna delar och skrivåtkomst testades inte",
"attention": "{count} av {total} lagringar behöver granskas",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"pool_integrity": {
@@ -5691,7 +5697,7 @@
"summary": {
"healthy": "Ceph rapporterar HEALTH_OK",
"degraded": "Ceph rapporterar {state}, med {count} namngivna kontroller",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"array_integrity": {
@@ -5700,7 +5706,7 @@
"summary": {
"intact": "De {total} uppsättningarna och kartorna behåller sin redundans",
"degraded": "{count} av {total} uppsättningar eller kartor saknar den",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
}
},
@@ -5712,7 +5718,7 @@
"withinLife": "De {total} diskavläsningarna ligger under femårströskeln",
"pastLife": "{count} av {total} diskavläsningar överskrider fem års drift",
"noReadings": "Ingen disk rapporterar användbara SMART-värden ({skipped} utan avläsningar)",
"evaluationFailed": "The check could not be evaluated"
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"disk_errors": {
@@ -5729,23 +5735,22 @@
"title": "Bond members",
"rationale": "MII-status för varje bond-medlem från /proc/net/bonding och hur många länkar som återstår. I active-backup rapporterar en reservmedlem som uppe och bär ingen trafik.",
"summary": {
"allUp": "All members of the {total} bond(s) are up",
"membersDown": "{count} bond member(s) are not up",
"evaluationFailed": "The check could not be evaluated"
"allUp": "Alla medlemmar av {total} obligationer är uppe",
"membersDown": "{count} obligationsmedlem (er) är inte upp",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
},
"bridge_without_ports": {
"title": "Bridge ports",
"rationale": "Portkonfigurationen för varje bridge. En bridge utan fysisk port betjänar ett internt eller routat nät.",
"summary": {
"allConnected": "The {total} bridge(s) carry a port",
"isolated": "{count} of {total} bridge(s) carry no port",
"evaluationFailed": "The check could not be evaluated"
"allConnected": "{total} bron bär en port",
"evaluationFailed": "Kontrollen kunde inte utvärderas"
}
}
}
},
"summaryFallback": "The check could not be evaluated",
"acceptRisk": {
"action": "Acceptera risken",
"revoke": "Återför till aktiva",
@@ -5779,17 +5784,15 @@
"OK": "OK"
},
"viewSwitch": {
"ariaLabel": "Switch between assessment and inventory",
"assessment": "Assessment",
"inventory": "Inventory",
"policy": "Policy",
"changes": "Ändringar"
"changes": "Ändringar",
"ariaLabel": "Växla mellan bedömning och lager"
},
"inventory": {
"loading": "Loading inventory…",
"failed": "The inventory could not be composed.",
"collectedAt": "Composed on {when}",
"unavailable": "Not read in this inventory",
"unresolved": "path not resolved",
"noUplink": "no uplink",
"identity": "Node identity",
@@ -5832,12 +5835,10 @@
"protection": "Backup",
"passthrough": "Passthrough",
"noBackup": "No backup",
"noBackupDetail": "No enabled backup job selects this guest.",
"applications": "Applications",
"versionUnknown": "version not detected",
"passthroughTitle": "PCI passthrough",
"iommuGroup": "IOMMU group {group}",
"sharedGroup": "{count} more device(s) in the same group",
"proxmenux": "ProxMenux optimizations",
"latency": "Nätverkslatens",
"subscriptionStatus": {
@@ -5848,7 +5849,11 @@
"suspended": "Pausad",
"new": "Väntar på aktivering",
"unknown": "Okänd"
}
},
"failed": "Inventeringen kunde inte komponeras.",
"unavailable": "Läs inte i denna inventering",
"noBackupDetail": "Inget aktiverat backup jobb väljer denna gäst.",
"sharedGroup": "{count} fler enheter (er) i samma grupp"
},
"profile": {
"label": "Report",
@@ -5871,7 +5876,6 @@
"area": "Area",
"inventoryAnnex": "Inventory annex",
"scopeTitle": "Scope of this report",
"scopeBody": "This report describes the Proxmox VE node named above, as observed from the node itself at the time stated. It does not cover the interior of the guests beyond what they declare, network equipment outside the host, physical infrastructure, or any dependency not visible from this node. Findings marked as not determined were not measured and are not evidence of absence.",
"building": "Rapporten sätts samman…",
"node": "Nod",
"profile": "Profil",
@@ -6020,11 +6024,12 @@
"structureSubtitle": "Hur {node} är byggd och konfigurerad",
"postureTitle": "Posture",
"posture": {
"security": "Host exposure and access.",
"backup": "Guest protection and whether it is real.",
"capacity": "Room to grow and the wear on the disks."
"security": "Värd exponering och åtkomst.",
"backup": "Gästskydd och om det är verkligt.",
"capacity": "Rum att växa och slitage på diskarna."
},
"scopeReadOnly": "Bedömningen inspekterar värdinställningar och hälsa. Den kan skriva rapporter och loggar; startstatuskontroller kan tillfälligt montera EFI-systempartitioner."
"scopeReadOnly": "Bedömningen inspekterar värdinställningar och hälsa. Den kan skriva rapporter och loggar; startstatuskontroller kan tillfälligt montera EFI-systempartitioner.",
"scopeBody": "Denna rapport beskriver Proxmox VE-noden som nämns ovan, som observerats från noden själv vid den tidpunkt som anges. Det täcker inte insidan av gästerna bortom vad de deklarerar, nätverksutrustning utanför värden, fysisk infrastruktur eller något beroende som inte är synligt från denna nod. Resultat som inte fastställts mättes inte och är inte bevis på frånvaro."
},
"results": "Resultat",
"classifications": {
@@ -6178,15 +6183,19 @@
"notApplicableScope": "Inget i det granskade omfånget som den här kontrollen gäller.",
"lynis": {
"title": "Run Lynis",
"bodyNotRun": "Lynis is installed but has not been run yet. Running it now completes the security review, but the process can take a few minutes.",
"bodyStale": "The Lynis report is {days} days old. You can run it now to refresh the data (it takes a little longer) or continue with the existing report.",
"withLynis": "Run with Lynis",
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
"cancel": "Cancel",
"bodyNotRun": "Lynis är installerad men har inte körts ännu. Kör det nu slutför säkerhetsgranskningen, men processen kan ta några minuter.",
"bodyStale": "Lynis-rapporten är {days} dagar gammal. Du kan köra det nu för att uppdatera data (det tar lite längre) eller fortsätta med den befintliga rapporten."
},
"readOnlyNotice": "Bedömningen inspekterar värdinställningar och hälsa. Den kan skriva rapporter och loggar; startstatuskontroller kan tillfälligt montera EFI-systempartitioner.",
"noActionNeeded": "Inget att göra. Denna kontroll fann vad den förväntar sig att hitta.",
"couldNotEvaluate": "Denna kontroll kunde inte utvärderas, så det rapporterar ingenting heller. Beviset registrerar vad det inte kunde läsa."
"couldNotEvaluate": "Denna kontroll kunde inte utvärderas, så det rapporterar ingenting heller. Beviset registrerar vad det inte kunde läsa.",
"neverRun": "Denna värd har inte bedömts ännu.",
"noFindings": "Inga resultat matchar det aktuella filtret.",
"acceptedNotice": "{count} accepterade risker registrerade på denna värd.",
"summaryFallback": "Kontrollen kunde inte utvärderas"
},
"runtime": {
"notifications": {
@@ -6298,8 +6307,8 @@
},
"migration_start": {
"title": "{hostname}: Migrering startade — {vmname} ({vmid})",
"body": "Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.",
"label": "Migreringen startade"
"label": "Migreringen startade",
"body": "Levande migration av {vmname} (ID: {vmid}) till nod {target_node} har börjat."
},
"migration_complete": {
"title": "{hostname}: Migreringen slutförd — {vmname} ({vmid})",
@@ -6468,8 +6477,8 @@
},
"node_disconnect": {
"title": "{hostname}: Nod {node_name} frånkopplad",
"body": "Node {node_name} has disconnected from the cluster.",
"label": "Noden frånkopplad"
"label": "Noden frånkopplad",
"body": "Nod {node_name} har kopplats från klustret."
},
"node_reconnect": {
"title": "{hostname}: Noden {node_name} återansluten",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ProxMenux-Monitor",
"version": "1.2.6.1-beta",
"version": "1.2.6.2-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ProxMenux-Monitor",
"version": "1.2.6.1-beta",
"version": "1.2.6.2-beta",
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ProxMenux-Monitor",
"version": "1.2.6.1-beta",
"version": "1.2.6.2-beta",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
+2
View File
@@ -136,6 +136,8 @@ cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || ec
cp "$SCRIPT_DIR/post_install_versions.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ post_install_versions.py not found"
cp "$SCRIPT_DIR/mount_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ mount_monitor.py not found"
cp "$SCRIPT_DIR/lxc_mount_points.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ lxc_mount_points.py not found"
cp "$SCRIPT_DIR/oci_console_logs.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_console_logs.py not found"
cp "$SCRIPT_DIR/oci_instance_info.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_instance_info.py not found"
cp "$SCRIPT_DIR/disk_temperature_history.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ disk_temperature_history.py not found"
cp "$SCRIPT_DIR/smartctl_resolver.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ smartctl_resolver.py not found"
cp "$SCRIPT_DIR/disk_identity.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ disk_identity.py not found"
+52 -99
View File
@@ -865,108 +865,28 @@ _PVE_OUR_HEADERS = {
}
def _ssl_cert_hostname(cert_path: str) -> str:
"""Pull the most useful hostname out of an x509 cert.
Preference order: first DNS SAN → CN. Returns '' on any failure.
Used to build a webhook URL that won't fail PVE's TLS verification
(issue #239 — PVE has no `--insecure` flag and the user's ACME cert
is bound to a hostname, not to `127.0.0.1`).
"""
try:
import subprocess
out = subprocess.run(
['openssl', 'x509', '-in', cert_path, '-noout',
'-ext', 'subjectAltName', '-subject'],
capture_output=True, text=True, timeout=5,
)
if out.returncode != 0:
return ''
text = out.stdout or ''
# SAN line example: " DNS:pve.example.com, DNS:pve, IP Address:..."
import re
for line in text.splitlines():
m = re.search(r'DNS:([A-Za-z0-9.\-]+)', line)
if m:
return m.group(1)
# CN fallback. "subject= CN = pve.example.com" or "...CN=pve.example.com"
m = re.search(r'CN\s*=\s*([A-Za-z0-9.\-]+)', text)
if m:
return m.group(1)
except Exception:
pass
return ''
def _hostname_resolves_locally(hostname: str) -> bool:
"""True when `hostname` resolves to one of this host's own IPs.
Anti-misconfig: refuse to build a webhook URL that points
elsewhere (a stale DNS entry pointing at the previous host, a CN
that names a different node in the cluster, etc.). PVE delivers
webhooks from the same node, so the URL has to round-trip to
ourselves.
"""
try:
import socket
import ipaddress
target_ips = set()
for info in socket.getaddrinfo(hostname, None):
ip = info[4][0]
target_ips.add(ipaddress.ip_address(ip).compressed)
# Collect our own IPs from /proc/net/fib_trie isn't portable; use
# psutil if available, otherwise fall back to socket on the
# hostname itself.
local_ips = {'127.0.0.1', '::1'}
try:
import psutil
for _iface, addrs in psutil.net_if_addrs().items():
for a in addrs:
if a.family in (socket.AF_INET, socket.AF_INET6):
local_ips.add(ipaddress.ip_address(a.address.split('%')[0]).compressed)
except Exception:
pass
return bool(target_ips & local_ips)
except Exception:
return False
# With HTTPS enabled, PVE delivers its notifications to a plain-HTTP listener
# the Monitor opens on loopback only for this route (see flask_server). PVE's
# webhook client validates certificates against the system CA store, which
# does not hold a self-signed, ACME-staging or PVE-CA-signed Monitor
# certificate, so an https target fails with "unable to get local issuer
# certificate" on every notification.
WEBHOOK_LOOPBACK_PORT = 8009
def _pve_webhook_url() -> str:
"""Return the URL we register with PVE as our webhook target.
Three branches:
1. SSL off → http://127.0.0.1:8008 (always works, no cert).
2. SSL on + cert hostname extractable and resolves locally →
https://<cert-hostname>:8008. This is what PVE's TLS layer
actually validates against. Without this, PVE rejects the
self/ACME cert with "IP address mismatch" (issue #239).
3. SSL on but hostname extraction/check failed → fall back to
https://127.0.0.1:8008 and accept that the user may still
hit the cert-mismatch error. We log so the operator can
diagnose. Better than silently emitting a wrong URL.
"""
"""Return the URL we register with PVE as our webhook target: the
Monitor itself over HTTP when SSL is off, and its loopback-only HTTP
listener when SSL is on. Both stay on 127.0.0.1, so nothing crosses
the network and no certificate is involved."""
try:
from auth_manager import load_ssl_config
cfg = load_ssl_config() or {}
if not cfg.get('enabled'):
return 'http://127.0.0.1:8008/api/notifications/webhook'
cert_path = cfg.get('cert_path') or ''
if cert_path:
host = _ssl_cert_hostname(cert_path)
if host and _hostname_resolves_locally(host):
return f'https://{host}:8008/api/notifications/webhook'
if host:
print(
f"[ProxMenux] webhook URL fallback to 127.0.0.1: "
f"cert hostname '{host}' does not resolve to a local "
f"IP — PVE will likely report a TLS verification "
f"error. Fix by ensuring the FQDN resolves on this "
f"host (e.g. /etc/hosts entry)."
)
return 'https://127.0.0.1:8008/api/notifications/webhook'
if cfg.get('enabled'):
return f'http://127.0.0.1:{WEBHOOK_LOOPBACK_PORT}/api/notifications/webhook'
except Exception:
return 'http://127.0.0.1:8008/api/notifications/webhook'
pass
return 'http://127.0.0.1:8008/api/notifications/webhook'
# Backward-compat alias for callers that read this at import time. Most
@@ -988,15 +908,48 @@ def _pve_read_file(path):
return None, str(e)
_PVE_BACKUPS_KEPT = 3
def _pve_backup_file(path):
"""Create timestamped backup if file exists. Never fails fatally."""
"""Create timestamped backup if file exists. Never fails fatally.
The backups live next to the file, inside the cluster filesystem, whose
size is limited: a copy identical to the newest one is not written again
and only the newest few are kept. Restore uses the newest.
"""
import os, shutil
from datetime import datetime
try:
if os.path.exists(path):
if not os.path.exists(path):
return
directory, name = os.path.split(path)
prefix = f"{name}.proxmenux_backup_"
existing = sorted(
(os.path.join(directory, f) for f in os.listdir(directory) if f.startswith(prefix)),
key=os.path.getmtime, reverse=True,
)
with open(path, 'rb') as f:
current = f.read()
newest_matches = False
if existing:
try:
with open(existing[0], 'rb') as f:
newest_matches = f.read() == current
except OSError:
pass
if not newest_matches:
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
backup = f"{path}.proxmenux_backup_{ts}"
shutil.copy2(path, backup)
shutil.copy2(path, os.path.join(directory, prefix + ts))
existing = sorted(
(os.path.join(directory, f) for f in os.listdir(directory) if f.startswith(prefix)),
key=os.path.getmtime, reverse=True,
)
for stale in existing[_PVE_BACKUPS_KEPT:]:
try:
os.remove(stale)
except OSError:
pass
except Exception:
pass
+143 -8
View File
@@ -678,6 +678,36 @@ def _warmup_lxc_ip_cache() -> int:
return count
def _lxc_isolated_ips(vmid):
"""Static addresses on interfaces that have no way out of the host.
lxc-info lists a container's addresses without saying which interface
carries each, so a container with a second network leg could be offered
at an address nobody can open: a ProxMenux stack wires its members through
a private bridge, and Paperless-ngx answered on 10.77.0.30 instead of its
LAN address. The interface a reader reaches is the one with a route out —
DHCP, or a static address with a gateway. A static address with no gateway
is local to the host, and is kept only as a last resort.
"""
try:
with open(f"/etc/pve/lxc/{int(vmid)}.conf", encoding="utf-8") as handle:
text = handle.read()
except (OSError, ValueError):
return set()
isolated = set()
for line in text.splitlines():
if line.startswith("["):
break # snapshot sections describe past states, not this one
match = re.match(r"net\d+:\s*(.*)", line)
if not match:
continue
options = dict(part.split("=", 1) for part in match.group(1).split(",") if "=" in part)
address = options.get("ip", "")
if address and address not in ("dhcp", "manual") and "gw" not in options:
isolated.add(address.split("/", 1)[0])
return isolated
def get_lxc_ip_from_lxc_info(vmid):
"""Get LXC IP addresses using lxc-info command (for DHCP containers)
Returns a dict with all IPs and classification"""
@@ -705,6 +735,9 @@ def get_lxc_ip_from_lxc_info(vmid):
else:
# Real network IPs (192.168.x.x, 10.x.x.x, etc.)
real_ips.append(ip)
isolated = _lxc_isolated_ips(vmid)
if isolated:
real_ips.sort(key=lambda ip: ip in isolated)
return {
'all_ips': ips,
@@ -3369,7 +3402,7 @@ def get_available_updates():
_system_info_cache['available_updates_stamp'] = stamp
return available_updates
# AGREGANDO FUNCIÓN PARA PARSEAR PROCESOS DE INTEL_GPU_TOP (SIN -J)
# Parse intel_gpu_top process output without -J.
def get_intel_gpu_processes_from_text():
"""Parse processes from intel_gpu_top text output (more reliable than JSON)"""
try:
@@ -3384,7 +3417,7 @@ def get_intel_gpu_processes_from_text():
bufsize=1
)
except FileNotFoundError:
# intel_gpu_top no está instalado, retornar lista vacía
# intel_gpu_top is not installed; return an empty list
return []
# Wait 2 seconds for intel_gpu_top to collect data
@@ -5519,7 +5552,7 @@ def _get_proxmox_storage_uncached():
for resource in resources:
node = resource.get('node', '')
# Filtrar solo storage del nodo local
# Keep only the local node storage
if node != local_node:
# print(f"[v0] Skipping storage {resource.get('storage')} from remote node: {node}")
pass
@@ -5538,7 +5571,7 @@ def _get_proxmox_storage_uncached():
pass
continue
# No filtrar storages no disponibles - mantenerlos para mostrar errores
# Keep unavailable storages so their errors stay visible
# Calcular porcentaje
percent = (used / total * 100) if total > 0 else 0.0
@@ -7832,7 +7865,7 @@ def get_detailed_gpu_info(gpu):
else:
# print(f"[v0] WARNING: No valid JSON objects found", flush=True)
pass
# CHANGE: Evitar bloqueo al leer stderr - usar communicate() con timeout
# communicate() with a timeout: reading stderr directly blocks
try:
# Use communicate() with timeout instead of read() to avoid blocking
_, stderr_output = process.communicate(timeout=0.5)
@@ -8312,8 +8345,7 @@ def get_detailed_gpu_info(gpu):
# print(f"[v0] Parsing fdinfo with {len(fdinfo)} entries", flush=True)
pass
# CHANGE: Corregir parseo de fdinfo con estructura anidada
# fdinfo es un diccionario donde las claves son los PIDs (como strings)
# fdinfo is nested: the keys are the PIDs, as strings
for pid_str, proc_data in fdinfo.items():
try:
process_info = {
@@ -13764,6 +13796,13 @@ def api_vm_apps_suggestions(vmid):
return jsonify({'error': str(e)}), 500
@app.route('/api/vms/<int:vmid>/apps/adguard-setup', methods=['GET'])
@require_auth
def api_vm_apps_adguard_setup(vmid):
import lxc_apps
return jsonify({'available': lxc_apps.oci_adguard_setup_available(vmid)})
@app.route('/api/vms/<int:vmid>/docker/inventory', methods=['GET'])
@require_auth
def api_vm_docker_inventory(vmid):
@@ -14113,6 +14152,8 @@ def _lxc_update_target_labels(
label = 'Applications'
elif target == 'docker-engine':
label = 'Docker Engine'
elif target == 'oci_image':
label = 'OCI image'
elif target.startswith('app:'):
app_item = apps.get(target.split(':', 1)[1]) or {}
label = str(app_item.get('name') or 'Application')
@@ -15077,7 +15118,7 @@ def api_vms_modal_cache_all():
return jsonify({'error': str(e)}), 500
# CHANGE: Modificar el endpoint para incluir la información completa de IPs
# The endpoint returns the complete IP information.
@app.route('/api/vms/<int:vmid>', methods=['GET'])
@require_auth
def get_vm_config(vmid):
@@ -15215,6 +15256,38 @@ def api_lxc_mount_points_runtime(vmid):
return jsonify({"ok": False, "error": str(e)}), 500
@app.route('/api/lxc/<int:vmid>/console-log', methods=['GET'])
@require_auth
def api_lxc_console_log(vmid):
"""Console output of a native OCI container. Never cached: the first
request returns the last `lines` lines, later ones pass back `offset`
and `inode` to receive only what was appended since."""
try:
import oci_console_logs
except ImportError as e:
return jsonify({"ok": False, "error": f"helper unavailable: {e}"}), 503
try:
lines = request.args.get('lines', default=200, type=int)
offset = request.args.get('offset', type=int)
inode = request.args.get('inode', type=int)
return jsonify(oci_console_logs.read(vmid, lines=lines, offset=offset, inode=inode))
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route('/api/lxc/<int:vmid>/oci-instance', methods=['GET'])
@require_auth
def api_lxc_oci_instance(vmid):
"""Whether the container was installed by OCI manager Apps, its stack,
host directories and pending operation, read from its record. Never
cached; it also says whether the console log exists (Logs tab)."""
try:
import oci_instance_info
return jsonify(oci_instance_info.info(vmid))
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route('/api/vms/<int:vmid>/logs', methods=['GET'])
@require_auth
def api_vm_logs(vmid):
@@ -22161,6 +22234,41 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
}
requested_target = sched.get("target") or "both"
if requested_targets == ['oci_image']:
# A container installed by OCI manager Apps is updated by replacing
# its image, through the same flow as the OCI menu, unattended.
command = ['bash', '/usr/local/share/proxmenux/scripts/oci/oci_manager_apps.sh',
'manage', str(vmid), '--action', 'update', '--unattended']
storage = (sched.get('backup_storage') or '').strip()
if sched.get('backup') and storage:
command += ['--keep-backup', storage]
if sched.get('acknowledge_external_data'):
command.append('--acknowledge-external-data')
delay = int(sched.get('release_delay_days') or 0)
if delay > 0:
command += ['--min-image-age-days', str(delay)]
try:
proc = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True, text=True,
timeout=4 * 3600, env=dict(os.environ, TERM='dumb'))
except subprocess.TimeoutExpired:
reasons.append('the OCI image update did not finish in time')
return finish('failure', 'oci_image', [])
output = re.sub(r'\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[()][A-Z0-9]|\r', '', (proc.stdout or '') + (proc.stderr or ''))
_append_lxc_update_log(log_path, output)
if proc.returncode == 0:
return finish('success', 'oci_image', ['oci_image'])
if proc.returncode == 5:
deferred_targets.append('oci_image')
reasons.append(f'the new image is younger than {delay} days')
return finish('deferred', 'oci_image', [])
if proc.returncode == 4:
reasons.append('the container has changes made outside ProxMenux; review them in OCI manager Apps')
return finish('skipped', 'oci_image', [])
if proc.returncode == 3:
reasons.append('another OCI operation was running')
return finish('skipped', 'oci_image', [])
reasons.append('the OCI image update failed; the previous installation is restored when the update started')
return finish('failure', 'oci_image', [])
if not os.path.isfile(_APPLY_UPDATES_SCRIPT):
reasons.append('update runner is not installed')
return finish('skipped', requested_target, [])
@@ -22744,6 +22852,18 @@ if __name__ == '__main__':
print(f"[ProxMenux] SSL config error, falling back to HTTP: {e}")
ssl_ctx = None
# With HTTPS on, PVE cannot validate the Monitor certificate against the
# system CA store, so its webhook is delivered to this plain-HTTP
# listener instead. It binds 127.0.0.1 only and answers nothing but the
# webhook route.
from flask_notification_routes import WEBHOOK_LOOPBACK_PORT
def _webhook_loopback_app(environ, start_response):
if environ.get('PATH_INFO') == '/api/notifications/webhook':
return app(environ, start_response)
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'Not found']
# Use gevent for SSL+WebSocket support, or fallback to Flask dev server
gevent_available = False
ssl_loaded = False
@@ -22842,11 +22962,26 @@ if __name__ == '__main__':
ssl_context=ssl_context
)
gevent_available = True
try:
webhook_server = pywsgi.WSGIServer(
('127.0.0.1', WEBHOOK_LOOPBACK_PORT), _webhook_loopback_app, log=None)
webhook_server.start()
print(f"[ProxMenux] PVE webhook listener on 127.0.0.1:{WEBHOOK_LOOPBACK_PORT}")
except Exception as _e:
print(f"[ProxMenux] WARN: PVE webhook listener could not start on "
f"127.0.0.1:{WEBHOOK_LOOPBACK_PORT} ({_e}); PVE notifications will not arrive", flush=True)
server.serve_forever()
except ImportError as e:
print(f"[ProxMenux] gevent not available ({e})")
# Fallback: Flask dev server with SSL - flask-sock handles WebSockets
ssl_context = auth_manager.create_reloadable_ssl_context(ssl_cert, ssl_key)
try:
from werkzeug.serving import make_server
_webhook_srv = make_server('127.0.0.1', WEBHOOK_LOOPBACK_PORT, _webhook_loopback_app, threaded=True)
threading.Thread(target=_webhook_srv.serve_forever, daemon=True).start()
print(f"[ProxMenux] PVE webhook listener on 127.0.0.1:{WEBHOOK_LOOPBACK_PORT}")
except Exception as _e:
print(f"[ProxMenux] WARN: PVE webhook listener could not start ({_e})", flush=True)
print("[ProxMenux] Starting Flask server with SSL (using flask-sock for WebSockets)...")
app.run(host='::', port=8008, debug=False, ssl_context=ssl_context, threaded=True)
else:
+1 -1
View File
@@ -199,7 +199,7 @@ def search_command():
'command': stripped
})
# Resetear descripciones para el siguiente comando
# Reset the descriptions for the next command
current_description = []
return jsonify({
+32 -9
View File
@@ -4146,7 +4146,8 @@ class HealthMonitor:
New thresholds:
- CASCADE: ≥15 errors (increased from 10)
- SPIKE: ≥5 errors AND 4x increase (more restrictive)
- PERSISTENT: Same error in 3 consecutive checks
- PERSISTENT: the same pattern seen in at least 3 checks whose
occurrences span 15+ minutes, and still present in this check
"""
cache_key = 'logs_analysis'
current_time = time.time()
@@ -4193,6 +4194,8 @@ class HealthMonitor:
recent_patterns = defaultdict(int)
previous_patterns = defaultdict(int)
# Patterns present in this check; each counts one check once.
seen_this_check = set()
critical_errors_found = {} # To store unique critical error lines for persistence
for line in recent_lines:
@@ -4363,10 +4366,15 @@ class HealthMonitor:
else:
self.persistent_log_patterns[pattern] = {
'count': 1,
'checks': 0,
'first_seen': current_time,
'last_seen': current_time,
'sample': line.strip()[:200], # Original line for display
}
if pattern not in seen_this_check:
seen_this_check.add(pattern)
self.persistent_log_patterns[pattern]['checks'] = \
self.persistent_log_patterns[pattern].get('checks', 0) + 1
for line in previous_lines:
if not line.strip():
@@ -4409,10 +4417,23 @@ class HealthMonitor:
samples.append(clean[:120])
return samples
# A pattern that stopped appearing is forgotten before the
# evaluation, and its own warning (if it had one) is closed.
patterns_to_remove = [
p for p, data in self.persistent_log_patterns.items()
if current_time - data['last_seen'] > 1800
]
for pattern in patterns_to_remove:
del self.persistent_log_patterns[pattern]
# Persistent means recurring: present in this check, seen in at
# least three checks, and with occurrences spanning 15 minutes.
# A burst that ended is not persistent however long ago it began.
persistent_errors = {}
for pattern, data in self.persistent_log_patterns.items():
time_span = current_time - data['first_seen']
if data['count'] >= 3 and time_span >= 900: # 15 minutes
span = data['last_seen'] - data['first_seen']
if (pattern in seen_this_check and data.get('checks', 1) >= 3
and data['count'] >= 3 and span >= 900):
persistent_errors[pattern] = data['count']
# Record as warning if not already recorded
@@ -4437,12 +4458,14 @@ class HealthMonitor:
'dismissable': True, 'occurrences': data['count']}
)
patterns_to_remove = [
p for p, data in self.persistent_log_patterns.items()
if current_time - data['last_seen'] > 1800
]
for pattern in patterns_to_remove:
del self.persistent_log_patterns[pattern]
# Close the per-pattern warnings of patterns that are no
# longer persistent: the burst ended, or it was forgotten.
for pattern in list(self.persistent_log_patterns.keys()) + patterns_to_remove:
if pattern in persistent_errors:
continue
stale_key = f'log_persistent_{hashlib.md5(pattern.encode()).hexdigest()[:8]}'
if health_persistence.is_error_active(stale_key, category='logs'):
health_persistence.clear_error(stale_key)
# B5 fix: Cap size to prevent unbounded memory growth under high error load
MAX_LOG_PATTERNS = 500
+393 -7
View File
@@ -29,6 +29,7 @@ import datetime
import copy
import concurrent.futures
import hashlib
import ipaddress
import json
import os
import re
@@ -36,6 +37,7 @@ import signal
import shlex
import socket
import subprocess
import sys
import threading
import time
import urllib.error
@@ -62,7 +64,7 @@ _UPSTREAM_CACHE_TTL_SEC = 24 * 3600
_VALID_METHODS = ("dpkg", "apk", "file", "binary",
"python_dist", "docker_label", "docker_exec",
"command", "manual")
"command", "manual", "oci_image")
_DETECTOR_FIELDS = (
"package", "file_path", "file_regex", "binary_path", "binary_args",
"python_path", "distribution", "container_name", "label",
@@ -93,7 +95,7 @@ _VALID_UPSTREAM_TYPES = ("github", "http_json", "docker_hub")
# exposes and the freeform "custom" text field.
_VALID_SCHEDULE_TARGETS = ("os", "app", "both")
_SCHEDULE_TARGET_ID_RE = re.compile(
r"^(?:os|apps|app:[A-Za-z0-9_-]{1,64}|docker-engine|docker-(?:compose|container):[A-Za-z0-9][A-Za-z0-9_.-]{0,127}|docker-unit:[a-f0-9]{20})$"
r"^(?:os|apps|oci_image|app:[A-Za-z0-9_-]{1,64}|docker-engine|docker-(?:compose|container):[A-Za-z0-9][A-Za-z0-9_.-]{0,127}|docker-unit:[a-f0-9]{20})$"
)
_BULK_TARGET_ID_RE = re.compile(
r"^(?:os|app:[A-Za-z0-9_-]{1,64}|docker-engine|docker-unit:[a-f0-9]{20})$"
@@ -886,6 +888,10 @@ def validate_schedule(payload: Any) -> tuple[bool, Any]:
"backup_storage": backup_storage,
"restart": restart,
"release_delay_days": release_delay_days,
# Host directories of an OCI container are not reverted by its
# backup; a scheduled image update runs only when this was
# confirmed when the schedule was saved.
"acknowledge_external_data": bool(payload.get("acknowledge_external_data")),
}
# Preserve `last_run_at` / `last_run_status` when the caller sent
# them (typical when the scheduler writes back after firing);
@@ -951,7 +957,12 @@ def validate_config(payload: dict) -> tuple[bool, Any]:
if method:
conf["installed_via"] = method
if method in ("dpkg", "apk"):
if method == "oci_image":
# Everything it needs is in the installation record ProxMenux wrote:
# the image, the digest and the registry. There is no field to fill
# and no upstream to configure.
pass
elif method in ("dpkg", "apk"):
pkg = (payload.get("package") or "").strip()
if not pkg or not _PACKAGE_RE.match(pkg):
return _err("package is required (letters/digits/._+@:/ up to 127 chars)")
@@ -1345,6 +1356,15 @@ def detect_installed_version(vmid, config: dict) -> tuple[Optional[str], Optiona
# tag_regex is reused (backward-compat with older hints).
pattern = config.get("installed_regex") or config.get("tag_regex") or r"(\d+[.\d]+)"
if method == "oci_image":
result = _oci_image_versions(vmid, with_latest=False)
if result.get("error"):
return None, result["error"]
# An image that states no application version is still an image with
# a build date and a digest, which is what its updates are decided on.
return (result.get("installed_version")
or _oci_image_label(None, result.get("image_created"), result.get("installed_digest"))), None
if method == "dpkg":
rc, out, err = _pct_exec(vmid, ["dpkg-query", "-W", "-f=${Version}", config["package"]])
if rc != 0:
@@ -3767,9 +3787,14 @@ def partition_scheduled_release_targets(
# A delegated app has no updater of its own and never resolves a
# release date, so including it would hold the whole schedule back
# waiting for a date that will never arrive.
# An application ProxMenux installed from an OCI image is updated by
# replacing its image through the OCI engine's own transaction, not by
# a helper or a command inside the guest, so a scripted plan has
# nothing it could run for it.
if (not app_id or app.get("managed_oci_app_id")
or app.get("helper_slug") == "docker"
or app.get("update_via") == "docker"):
or app.get("update_via") == "docker"
or app.get("installed_via") == "oci_image"):
continue
if not select_all_apps and app_id not in selected_app_ids:
continue
@@ -3962,13 +3987,20 @@ def _app_update_notification_payload(vmid, app: dict) -> Optional[dict]:
return None
state = app.get("state") or {}
latest = state.get("latest_version")
installed = state.get("installed_version")
if app.get("installed_via") == "oci_image" and state.get("latest_digest"):
# What is published is an image. Naming it by version, build date and
# digest keeps two rebuilds of the same version from being taken for
# one notification, and tells the reader what actually changed.
latest = _oci_image_label(latest, state.get("latest_image_created"), state.get("latest_digest"))
installed = _oci_image_label(installed, state.get("image_created"), state.get("installed_digest"))
if not state.get("update_available") or not latest:
return None
return {
"vmid": int(vmid),
"ct_name": app.get("name") or f"CT-{vmid}",
"app_name": app.get("name") or "app",
"installed": state.get("installed_version") or "unknown",
"installed": installed or "unknown",
"latest": latest,
"app_id": str(app.get("id") or ""),
}
@@ -4291,6 +4323,28 @@ def check_app(
except (ValueError, TypeError):
pass
if app.get("installed_via") == "oci_image":
result = _oci_image_versions(vmid, known=state)
app["state"] = {
"installed_version": result.get("installed_version"),
"latest_version": result.get("latest_version"),
"latest_published_at": None,
"update_available": result.get("update_available"),
"error": result.get("error"),
"checked_at": _now_iso(),
"installed_digest": result.get("installed_digest"),
"latest_digest": result.get("latest_digest"),
"image_created": result.get("image_created"),
"latest_image_created": result.get("latest_image_created"),
"image_reference": result.get("image_reference"),
"image_repository": result.get("image_repository"),
}
sidecar["updated_at"] = _now_iso()
_write_sidecar(vmid, sidecar)
if notify and app["state"]["update_available"] and app["state"]["latest_version"]:
_fire_update_notification(vmid, app)
return sidecar
installed, inst_err, _healed = _detect_with_alt_healing(vmid, app)
# Trigger the upstream fetch when ANY upstream source is
# configured. The dispatcher inside `fetch_latest_upstream`
@@ -4511,6 +4565,12 @@ def _summarise_app(app: dict) -> dict:
# match this registered app against the CT's helper_slug and
# display its installed/upstream versions.
"helper_slug": app.get("helper_slug") or "",
# OCI image identity, for the Updates tab of an OCI container.
"image_reference": state.get("image_reference"),
"image_created": state.get("image_created"),
"installed_digest": state.get("installed_digest"),
"latest_image_created": state.get("latest_image_created"),
"latest_digest": state.get("latest_digest"),
}
@@ -5238,6 +5298,276 @@ def _helper_slug_meta(vmid) -> Optional[dict]:
return None
_OCI_INSTANCE_ROOT = "/usr/local/share/proxmenux/oci/instances"
_OCI_CATALOG_INDEX = "/usr/local/share/proxmenux/oci/engine/catalog/index.json"
_oci_catalog_cache: tuple[float, dict] | None = None
def _oci_catalog_icons() -> dict:
"""Current icon per catalog application, keyed by template id.
The installation record keeps a copy of the catalog entry as it stood on
the day of the install, which freezes the icon along with everything else.
Icons get corrected — most of the catalog used to point at a URL that
answered 404 — so the panel reads the catalog and keeps the record as the
fallback for an application the catalog no longer lists.
"""
global _oci_catalog_cache
now = time.time()
if _oci_catalog_cache and now - _oci_catalog_cache[0] < 600:
return _oci_catalog_cache[1]
icons: dict = {}
try:
with open(_OCI_CATALOG_INDEX, encoding="utf-8") as handle:
for item in (json.load(handle) or {}).get("applications", []):
icon = (item or {}).get("icon")
if not isinstance(icon, str) or not icon.startswith("http"):
continue
# An installation records the template id; the catalog is keyed
# by the application id and carries both.
for key in (item.get("template_id"), item.get("id")):
if key:
icons.setdefault(key, icon)
except (OSError, ValueError, TypeError):
pass
_oci_catalog_cache = (now, icons)
return icons
def _oci_localised(value) -> str:
"""A catalog_ui text field, which is either a string or a locale map."""
if isinstance(value, str):
return value.strip()
if isinstance(value, dict):
for key in ("en_US", "en", *sorted(value)):
text = value.get(key)
if isinstance(text, str) and text.strip():
return text.strip()
return ""
def _oci_name_from_image(reference: str | None) -> str:
"""A presentable name for an image that carries no catalog entry.
A stack member records the image it runs and the role it plays, not a
title: `nextcloud:latest` as the `application` of a Nextcloud stack.
"""
repository = str(reference or "").split("@", 1)[0]
basename = repository.rsplit("/", 1)[-1].rsplit(":", 1)[0].strip()
if not basename:
return ""
return " ".join(word.capitalize() for word in re.split(r"[-_.]+", basename) if word)
def _oci_instance_meta(vmid) -> Optional[dict]:
"""What ProxMenux itself recorded when it installed this container.
The sibling of `_helper_slug_meta`, and stronger evidence: a helper slug
is a hint read back out of the guest, while this is the contract the
installer wrote. It needs no `pct exec`, so it also answers for a stopped
container, and it cannot mistake an incidental binary for the application
— CT 152 runs Chromium and happens to have Docker inside, which the
runtime probe reported as the only candidate.
"""
try:
with open(f"{_OCI_INSTANCE_ROOT}/{int(vmid)}/oci-compose.json", encoding="utf-8") as handle:
record = json.load(handle)
except (OSError, ValueError, TypeError):
return None
if not isinstance(record, dict) or record.get("status") not in ("installed", "assembling"):
return None
template = record.get("template") or {}
contract = template.get("container_contract") or {}
image = contract.get("image") or {}
observed_image = (record.get("observed") or {}).get("image") or {}
# A stack member carries only its own image contract; the presentation
# belongs to the stack it is part of, which records its title, site,
# category and the endpoint the stack is reached on.
stack = record.get("stack") if isinstance(record.get("stack"), dict) else {}
stack_template = stack.get("template") if isinstance(stack.get("template"), dict) else {}
role = str((record.get("stack_member") or {}).get("name") or "").strip()
member = record.get("stack_member") or {}
is_primary = not stack_template or member.get("primary_vmid") in (None, record.get("vmid"))
ui = template.get("catalog_ui") or stack_template.get("catalog_ui") or {}
if not template.get("catalog_ui") and stack_template and is_primary:
# Only the member the stack is reached on inherits the stack endpoint.
# The cache and the database of a Nextcloud stack do not answer on its
# web port, and offering it would register a service that is not there.
template = {**stack_template, "id": template.get("id") or stack_template.get("id")}
elif not template.get("catalog_ui") and stack_template:
ui = {key: value for key, value in ui.items()
if key in ("category", "category_label")}
# `container_contract.ports` lists every port the image exposes; the
# endpoint is the one the application is actually reached on, with its
# scheme. Chromium exposes 3000 and 3001 and serves on 3001 over https.
endpoint = next((e for e in (template.get("first_run") or {}).get("endpoints") or []
if isinstance(e, dict) and e.get("port")), None) or ui.get("launch") or {}
ports = []
for entry in contract.get("ports") or []:
try:
port = int((entry or {}).get("container_port"))
except (TypeError, ValueError):
continue
if 1 <= port <= 65535 and port not in ports:
ports.append(port)
template_id = str(template.get("id") or "").strip()
catalog_icons = _oci_catalog_icons()
logo = catalog_icons.get(template_id) or ""
if not logo:
# A stack or a one-off image has no catalog entry of its own, but the
# image it runs usually does: the Nextcloud stack wears Nextcloud's.
repository = str(image.get("reference") or "").split("@", 1)[0]
basename = repository.rsplit("/", 1)[-1].rsplit(":", 1)[0].strip().lower()
logo = catalog_icons.get(basename) or ""
if not logo:
logo = ui.get("icon") if isinstance(ui.get("icon"), str) else ""
website = ui.get("website") if isinstance(ui.get("website"), str) else ""
return {
"template_id": template_id or None,
"name": (_oci_localised(ui.get("title"))
or _oci_name_from_image(image.get("reference"))
or str(template.get("id") or "").strip() or None),
"role": role or None,
"logo": logo if logo.startswith(("http://", "https://")) else "",
"website": website if website.startswith(("http://", "https://")) else "",
"category": str(ui.get("category") or "").strip() or None,
"category_label": str(ui.get("category_label") or "").strip() or None,
"image_reference": str(image.get("reference") or "").strip() or None,
# The repository of the image — its GitHub project, or its page on the
# registry for an official image — which is where its updates come from.
"repository": (str(ui.get("repository") or "").strip()
or str((template.get("source") or {}).get("repository") or "").strip() or None),
"endpoint_port": endpoint.get("port") if isinstance(endpoint.get("port"), int) else None,
"endpoint_scheme": str(endpoint.get("scheme") or "").strip().lower() or None,
"endpoint_path": str(endpoint.get("path") or "").strip() or None,
"ports": ports,
# The exact image this container was created from. Its digest is what
# an update is decided on; the version label is only for reading.
"installed_digest": str(observed_image.get("manifest_digest") or "").strip() or None,
"architecture": str(observed_image.get("architecture") or "").strip() or None,
}
def oci_adguard_setup_available(vmid) -> bool:
"""Probe only this OCI application's setup endpoint, without caching it."""
meta = _oci_instance_meta(vmid)
if not meta or meta.get('template_id') != 'image-adguard-home':
return False
try:
result = subprocess.run(['lxc-info', '-n', str(int(vmid)), '-iH'],
capture_output=True, text=True, timeout=3, check=True)
ip = next(str(ipaddress.IPv4Address(value.strip()))
for value in result.stdout.splitlines()
if value.strip() and ipaddress.ip_address(value.strip()).version == 4)
with socket.create_connection((ip, 3000), timeout=1) as connection:
connection.settimeout(1)
connection.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
return connection.recv(32).startswith(b'HTTP/')
except (OSError, ValueError, StopIteration, subprocess.SubprocessError):
return False
_OCI_REMOTE_DIR = "/usr/local/share/proxmenux/oci/engine/remote"
def _oci_state_module():
"""The OCI engine's own reader of image versions.
Reused rather than copied: it resolves the platform manifest, reads the
version the image states in its environment before the label it may
have inherited from its base, and it is the same code the engine
installs and updates with, so the panel and the updater cannot disagree
about what an image is.
"""
if _OCI_REMOTE_DIR not in sys.path:
sys.path.insert(0, _OCI_REMOTE_DIR)
import oci_installation_state
return oci_installation_state
def _oci_repository(reference: str) -> str:
repository = reference.split("@", 1)[0]
if ":" in repository.rsplit("/", 1)[-1]:
repository = repository.rsplit(":", 1)[0]
return repository
def _oci_resolve(module, reference: str, architecture: str) -> dict:
# One retry: an anonymous registry answers the occasional request with an
# error that the next one does not repeat.
try:
return module.resolve_candidate(reference, architecture)
except RuntimeError:
time.sleep(2)
return module.resolve_candidate(reference, architecture)
def _oci_image_versions(vmid, known: Optional[dict] = None, with_latest: bool = True) -> dict:
"""Installed and published version of a container ProxMenux installed.
Nothing is inferred. The installation record names the image and the
exact digest the container was created from; the registry says which
digest the same tag points at today. An update is available when those
two differ — the version strings only say which one it is, and they are
not compared, because a rebuild can keep its number and a build id such
as ``b1ee1dc8-ls55`` has no order to compare.
The installed version is read by digest, which never changes, so a
previous answer for the same digest is reused instead of asked again.
"""
meta = _oci_instance_meta(vmid)
if not meta or not meta.get("image_reference") or not meta.get("installed_digest"):
return {"error": "no OCI installation record for this container"}
reference = meta["image_reference"]
architecture = meta.get("architecture") or "amd64"
installed_digest = meta["installed_digest"]
result: dict = {"installed_digest": installed_digest, "image_reference": reference,
"image_repository": meta.get("repository")}
try:
module = _oci_state_module()
except Exception as exc:
return {**result, "error": f"OCI engine unavailable: {exc}"}
known = known or {}
if (known.get("installed_digest") == installed_digest
and (known.get("installed_version") or known.get("image_created"))):
result["installed_version"] = known.get("installed_version")
result["image_created"] = known.get("image_created")
else:
try:
installed = _oci_resolve(
module, f"{_oci_repository(reference)}@{installed_digest}", architecture)
result["installed_version"] = installed.get("version")
result["image_created"] = installed.get("created")
except Exception as exc:
return {**result, "error": f"could not read the installed image: {exc}"}
if not with_latest:
return result
try:
latest = _oci_resolve(module, reference, architecture)
except Exception as exc:
return {**result, "error": f"could not read {reference} from its registry: {exc}"}
latest_digest = latest.get("manifest_digest")
# The image decides. An application whose version did not move can still
# have a new image — a rebuild on a patched base — and that is an update
# for a container whose application only changes when its image does.
replaced = bool(latest_digest) and latest_digest != installed_digest
result.update(latest_digest=latest_digest, latest_version=latest.get("version"),
latest_image_created=latest.get("created"), update_available=replaced)
return result
def _oci_image_label(version: Optional[str], created: Optional[str], digest: Optional[str]) -> str:
"""One image, as a line a reader can compare: version, build date, digest."""
parts = [version] if version else []
if created:
parts.append(str(created)[:10])
if digest:
parts.append(str(digest).split(":", 1)[-1][:8])
return " · ".join(parts) or "unknown"
def _catalog_lookup(slug: str) -> Optional[dict]:
"""Fetch the community-scripts catalog entry for a slug.
Returns {name, updateable, default_port, logo} or None.
@@ -5292,6 +5622,7 @@ def get_suggestions(vmid, force: bool = False) -> dict:
if p in _KNOWN_WEB_PORTS:
web_hint = "/"
break
oci_meta = _oci_instance_meta(vmid)
meta = _helper_slug_meta(vmid) or {}
slug = meta.get("slug")
# Suppress base-OS helper slugs from the suggestion pipeline.
@@ -5318,7 +5649,10 @@ def get_suggestions(vmid, force: bool = False) -> dict:
detector.get("installed_via") in ("docker_label", "docker_exec")
for detector in primary_matches
)
if "docker" in detected_map and primary_is_docker_workload:
# An OCI container installed by ProxMenux is the application named in its
# own record. Promoting a probed binary over that would offer Docker for a
# Chromium container just because the image ships a docker client.
if "docker" in detected_map and primary_is_docker_workload and not oci_meta:
slug = "docker"
meta = {"slug": "docker", "name": "Docker"}
# Tracking hint pipeline: catalog + curated hints merged.
@@ -5560,6 +5894,47 @@ def get_suggestions(vmid, force: bool = False) -> dict:
"tracking_suggestion": det_tracking,
})
if oci_meta:
# The record states what this container runs, so a probe finding is
# noise: CT 152 runs Chromium and ships a docker client, and offering
# to register Docker there invites the user to track the wrong thing.
extras = []
# Recorded facts replace every probed guess: the name, the logo, the
# site and the endpoint the application is served on. The version is
# not filled here — the image has no detector among the guest-side
# methods — so the entry registers without version tracking until the
# OCI detector exists.
name_sug = oci_meta["name"] or name_sug
logo_url = oci_meta["logo"] or logo_url
category_suggestion = oci_meta["category_label"] or suggest_category_for(slug)
if oci_meta["endpoint_port"]:
default_ports = [oci_meta["endpoint_port"]]
ports = [oci_meta["endpoint_port"]] + [p for p in (oci_meta["ports"] or ports)
if p != oci_meta["endpoint_port"]]
elif oci_meta["ports"]:
default_ports = list(oci_meta["ports"])
web_hint = oci_meta["endpoint_path"] or web_hint
if oci_meta["template_id"] == "image-adguard-home":
# The first-run endpoint disappears once setup switches to :80.
default_ports = [80]
ports = [80, 3000]
# Version tracking comes with the registration. Its updates are
# decided by the image, which always has a build date and a digest,
# so it applies even to an image that states no application version.
versions = _oci_image_versions(vmid, with_latest=False)
if not versions.get("error"):
tracking = {
"installed_via": "oci_image",
"detected_version": versions.get("installed_version") or _oci_image_label(
None, versions.get("image_created"), versions.get("installed_digest")),
"detector_verified": True,
"detector_source": "oci_instance_record",
"logo": logo_url or None,
"website": oci_meta["website"] or None,
}
else:
category_suggestion = suggest_category_for(slug)
return {
"name_suggestion": name_sug,
"helper_slug": slug,
@@ -5568,9 +5943,20 @@ def get_suggestions(vmid, force: bool = False) -> dict:
"tracking_suggestion": tracking,
"default_ports": default_ports,
"logo_url": logo_url or None,
# Identity of a ProxMenux OCI install: the scheme the endpoint is
# served on, the image it was created from, and the upstream source.
"oci_instance": {
"template_id": oci_meta["template_id"],
"image_reference": oci_meta["image_reference"],
"repository": oci_meta["repository"],
"scheme": oci_meta["endpoint_scheme"],
"port": oci_meta["endpoint_port"],
"path": oci_meta["endpoint_path"],
"website": oci_meta["website"],
} if oci_meta else None,
# Categoría preset for the primary detection — same lookup as
# get_catalog_entry so the Register button pre-selects it.
"category": suggest_category_for(slug),
"category": category_suggestion,
"extras": extras,
"docker_workloads": sorted(docker_workloads, key=lambda item: item["name"].lower()),
"docker_web_links": docker_web_links,
+46 -11
View File
@@ -304,21 +304,56 @@ def _df_via_host_pid(host_pid: str, ct_target: str) -> dict[str, Optional[int]]:
["df", "-B1", "--output=size,used,avail", full],
capture_output=True, text=True, timeout=_STAT_TIMEOUT,
)
except subprocess.TimeoutExpired:
# A filesystem that does not answer df will not answer statfs
# either; asking again would only double the wait.
return empty
except OSError:
return empty
if proc.returncode == 0:
lines = [ln for ln in proc.stdout.strip().splitlines() if ln.strip()]
parts = lines[-1].split() if len(lines) >= 2 else []
if len(parts) >= 3:
try:
return {
"total_bytes": int(parts[0]),
"used_bytes": int(parts[1]),
"available_bytes": int(parts[2]),
}
except ValueError:
pass
return _statfs_via_host(full)
def _statfs_via_host(full: str) -> dict[str, Optional[int]]:
"""Capacity of a path read from its filesystem rather than the mount table.
df names a path by finding the mount that holds it in the host's own
table, and a volume of a native OCI container is not in that table: df
answers "no file systems processed" and the tab showed no usage for any
of its volumes. statfs asks the filesystem of the path directly and
returns the same three figures df prints. It is only reached when df
completes without an answer, so every mount df can measure keeps the
value it had.
"""
empty = {"total_bytes": None, "used_bytes": None, "available_bytes": None}
try:
proc = subprocess.run(
["stat", "-f", "-c", "%S %b %f %a", full],
capture_output=True, text=True, timeout=_STAT_TIMEOUT,
)
if proc.returncode != 0:
return empty
lines = [ln for ln in proc.stdout.strip().splitlines() if ln.strip()]
if len(lines) < 2:
return empty
parts = lines[-1].split()
if len(parts) < 3:
return empty
return {
"total_bytes": int(parts[0]),
"used_bytes": int(parts[1]),
"available_bytes": int(parts[2]),
}
block, total, free, available = (int(value) for value in proc.stdout.split())
except (subprocess.TimeoutExpired, OSError, ValueError):
return empty
if block <= 0 or total <= 0:
return empty
return {
"total_bytes": total * block,
"used_bytes": (total - free) * block,
"available_bytes": available * block,
}
def _df_via_pct_exec(vmid: str, ct_target: str,
+1 -2
View File
@@ -97,8 +97,7 @@ class RateLimiter:
# Counter of events dropped while over the rate limit. Surfaced via
# `consume_drop_count()` so the dispatch loop can periodically log
# "X events suppressed by rate-limit" instead of letting them
# disappear silently. Audit Tier 6 — `RateLimiter` descarta
# silenciosamente eventos sobre el límite.
# disappear silently.
self._dropped: int = 0
def allow(self) -> bool:
+1 -2
View File
@@ -2502,8 +2502,7 @@ class TaskWatcher:
# Manual starts (onboot=0) within the grace period also bypass the
# aggregator: a user manually starting a VM right after boot wants
# the individual confirmation, not their action silently rolled into
# the autostart summary. Audit Tier 6 — `system_startup` aggregation
# puede tragar VM starts manuales del usuario durante grace period.
# the autostart summary.
_STARTUP_EVENTS = {'vm_start', 'ct_start'}
if event_type in _STARTUP_EVENTS and not is_error and not is_warning:
if _shared_state.is_startup_period():
+1 -2
View File
@@ -1353,8 +1353,7 @@ TEMPLATES = {
# `system-mail` event, and the Monitor forwards it to every enabled
# channel. Most operators want smartd alerts but NOT noisy cron
# output — without a visible toggle the only fix is editing
# /etc/aliases or removing MAILTO from the cron job. Audit Tier 6
# — `system_mail` toggle no visible en UI / reportado por usuario.
# /etc/aliases or removing MAILTO from the cron job.
},
'apt_listchanges': {
'title': '{hostname}: {pve_title}',
+132
View File
@@ -0,0 +1,132 @@
"""Console output of a native OCI container, read from the host.
ProxMenux creates every OCI container with `lxc.console.logfile` pointing at
`/var/log/proxmenux/oci/<vmid>.console.log`: liblxc copies the stdout and
stderr of the image's entrypoint there, the way `docker logs` keeps them. The
file already exists on the host, so nothing here runs inside the container.
The path is derived from the VMID and confirmed against the container's own
configuration; no caller can name a file. Reads are bounded — the last lines
on first open, then only what was appended since the offset the viewer holds —
and a file that got shorter or was replaced (logrotate's copytruncate, a
container rebuilt by an update) resets the viewer instead of returning garbage.
"""
from __future__ import annotations
import os
import re
LOG_DIR = "/var/log/proxmenux/oci"
TAIL_READ_LIMIT = 2 * 1024 * 1024 # bytes scanned to find the last lines
FOLLOW_READ_LIMIT = 512 * 1024 # bytes returned per follow request
MAX_LINES = 2000
# CSI sequences (colours, cursor), OSC sequences (window titles) and the lone
# ESC-letter codes some programs emit. Stripped, never rendered: the text goes
# to the page as text.
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]")
_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
def log_path(vmid: int) -> str:
return os.path.join(LOG_DIR, f"{int(vmid)}.console.log")
def configured_log(vmid: int) -> str | None:
"""The console log the container declares, when it is the ProxMenux one."""
try:
with open(f"/etc/pve/lxc/{int(vmid)}.conf", encoding="utf-8") as handle:
for line in handle:
if line.startswith("["):
break
if line.startswith("lxc.console.logfile:"):
value = line.split(":", 1)[1].strip()
return value if value == log_path(vmid) else None
except (OSError, ValueError):
return None
return None
def terminal_state(vmid: int) -> dict:
"""Whether the Proxmox console of the container opens a shell."""
mode = "tty"
try:
with open(f"/etc/pve/lxc/{int(vmid)}.conf", encoding="utf-8") as handle:
for line in handle:
if line.startswith("["):
break
if line.startswith("cmode:"):
mode = line.split(":", 1)[1].strip()
except (OSError, ValueError):
pass
return {"cmode": mode, "shell": mode == "shell"}
def clean(text: str) -> list[str]:
"""Readable lines from raw console output.
CRLF becomes a line end. A lone carriage return is how a progress bar
redraws itself in place, so only what was written after the last one on a
line is kept — the state the terminal would have shown.
"""
text = _ANSI_RE.sub("", text).replace("\r\n", "\n")
lines = []
for raw in text.split("\n"):
if "\r" in raw:
raw = raw.rsplit("\r", 1)[-1]
lines.append(_CONTROL_RE.sub("", raw))
return lines
def _read_tail(handle, size: int, lines: int) -> tuple[list[str], bool]:
start = max(0, size - TAIL_READ_LIMIT)
handle.seek(start)
data = handle.read(size - start).decode("utf-8", errors="replace")
result = clean(data)
if start > 0 and result:
result = result[1:] # the first line was cut by the read window
if result and result[-1] == "":
result = result[:-1]
head_cut = start > 0 or len(result) > lines
return result[-lines:], head_cut
def read(vmid: int, lines: int = 200, offset: int | None = None, inode: int | None = None) -> dict:
"""The last `lines` lines, or what was appended after `offset`.
`lines=0` reads nothing: it only says whether the container has a
console log, which is what decides if the viewer is offered at all.
"""
lines = max(0, min(int(lines), MAX_LINES))
path = configured_log(vmid)
base = {"ok": True, "vmid": int(vmid), **terminal_state(vmid)}
if not path:
return {**base, "enabled": False, "lines": [], "size": 0, "offset": 0, "inode": None}
try:
stat = os.stat(path)
except OSError:
return {**base, "enabled": True, "path": path, "lines": [], "size": 0, "offset": 0, "inode": None}
size = stat.st_size
base.update(enabled=True, path=path, size=size, inode=stat.st_ino)
if lines == 0 and offset is None:
return {**base, "lines": [], "offset": size}
with open(path, "rb") as handle:
replaced = inode is not None and inode != stat.st_ino
if offset is None or replaced or offset > size:
# First open, or the file the viewer followed is gone: rotated by
# copytruncate, or recreated with the container.
result, head_cut = _read_tail(handle, size, max(lines, 1))
return {**base, "lines": result, "offset": size, "reset": offset is not None,
"head_truncated": head_cut}
handle.seek(offset)
chunk = handle.read(min(size - offset, FOLLOW_READ_LIMIT))
# Only whole lines are returned; an unfinished last line is left for the
# next read, where it arrives complete.
cut = chunk.rfind(b"\n")
if cut < 0:
return {**base, "lines": [], "offset": offset, "reset": False}
complete = chunk[:cut + 1]
result = clean(complete.decode("utf-8", errors="replace"))
if result and result[-1] == "":
result = result[:-1]
return {**base, "lines": result, "offset": offset + len(complete), "reset": False}
+62
View File
@@ -0,0 +1,62 @@
"""What the VM & LXC modal needs to know about an OCI instance.
Read-only view of the installation record OCI manager Apps keeps for every
container it created: whether the container is one, whether it belongs to a
multi-container application, whether it uses host directories (which its
backup does not revert) and whether an operation is pending. Nothing here
changes the record or runs inside the container.
"""
from __future__ import annotations
import json
import os
import oci_console_logs
ROOT = "/usr/local/share/proxmenux/oci/instances"
def _record(vmid: int) -> dict | None:
path = os.path.join(ROOT, str(int(vmid)), "oci-compose.json")
try:
with open(path, encoding="utf-8") as handle:
record = json.load(handle)
except (OSError, ValueError):
return None
return record if isinstance(record, dict) else None
def _host_dirs(record: dict) -> bool:
mounts = (record.get("deployment") or {}).get("mounts") or []
return any(isinstance(m, dict) and m.get("type") == "host-bind" for m in mounts)
def info(vmid: int) -> dict:
vmid = int(vmid)
result = {
"vmid": vmid,
"oci_instance": False,
"console_log": oci_console_logs.configured_log(vmid) is not None,
"stack": False,
"primary_vmid": vmid,
"members": [],
"host_directories": False,
"pending": False,
}
record = _record(vmid)
if record is None:
return result
primary_id = int((record.get("stack_member") or {}).get("primary_vmid") or vmid)
primary = record if primary_id == vmid else (_record(primary_id) or {})
members = [int(m["vmid"]) for m in (primary.get("stack") or {}).get("members") or []
if isinstance(m, dict) and str(m.get("vmid", "")).isdigit()]
records = [record] if not members else [r for r in (_record(m) for m in members) if r]
result.update(
oci_instance=True,
stack=bool(members) or bool(record.get("stack_member")),
primary_vmid=primary_id,
members=members,
host_directories=any(_host_dirs(r) for r in records),
pending=bool(record.get("pending_transaction") or primary.get("pending_stack_transaction")),
)
return result
@@ -0,0 +1,217 @@
import importlib.util
import json
import shutil
import socket
import ssl
import subprocess
import tempfile
import threading
import unittest
from pathlib import Path
from unittest import mock
MODULE_PATH = Path(__file__).resolve().parents[1] / "auth_manager.py"
SPEC = importlib.util.spec_from_file_location("auth_manager_ssl_under_test", MODULE_PATH)
auth_manager = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(auth_manager)
class _FakeSslSocket:
def __init__(self, context):
self.context = context
class ProxmoxCertificateHotReloadTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
if shutil.which("openssl") is None:
raise unittest.SkipTest("openssl is required for TLS fixture generation")
cls.fixture_dir = tempfile.TemporaryDirectory()
fixture_path = Path(cls.fixture_dir.name)
cls.pairs = []
for name in ("original", "renewed"):
cert_path = fixture_path / f"{name}.pem"
key_path = fixture_path / f"{name}.key"
subprocess.run(
[
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-nodes", "-days", "1", "-subj", f"/CN={name}.test",
"-keyout", str(key_path), "-out", str(cert_path),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
cls.pairs.append((cert_path, key_path))
@classmethod
def tearDownClass(cls):
cls.fixture_dir.cleanup()
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.temp_dir.cleanup)
temp_path = Path(self.temp_dir.name)
self.active_cert = temp_path / "pveproxy-ssl.pem"
self.active_key = temp_path / "pveproxy-ssl.key"
self.ssl_config = temp_path / "ssl_config.json"
self._install_pair(0)
self._write_config("proxmox")
self.patch = mock.patch.multiple(
auth_manager,
SSL_CONFIG_FILE=self.ssl_config,
PROXMOX_CUSTOM_CERT_PATH=str(self.active_cert),
PROXMOX_CUSTOM_KEY_PATH=str(self.active_key),
PROXMOX_CERT_PATH=str(temp_path / "missing-pve-ssl.pem"),
PROXMOX_KEY_PATH=str(temp_path / "missing-pve-ssl.key"),
)
self.patch.start()
self.addCleanup(self.patch.stop)
self.addCleanup(self._reset_runtime)
def _reset_runtime(self):
with auth_manager._SSL_RUNTIME_LOCK:
auth_manager._SSL_RUNTIME_CONTEXT = None
auth_manager._SSL_RUNTIME_FINGERPRINT = ""
auth_manager._SSL_RUNTIME_CERT_PATH = ""
auth_manager._SSL_RUNTIME_KEY_PATH = ""
auth_manager._SSL_RUNTIME_SOURCE = "none"
auth_manager._SSL_RUNTIME_LAST_REFRESH_ERROR = ""
def _install_pair(self, index):
cert_path, key_path = self.pairs[index]
shutil.copyfile(cert_path, self.active_cert)
shutil.copyfile(key_path, self.active_key)
def _write_config(self, source):
self.ssl_config.write_text(json.dumps({
"enabled": True,
"cert_path": str(self.active_cert),
"key_path": str(self.active_key),
"source": source,
}))
def _create_context(self):
return auth_manager.create_reloadable_ssl_context(
str(self.active_cert), str(self.active_key)
)
def test_unchanged_pair_does_not_rebuild_the_context(self):
context = self._create_context()
ssl_socket = _FakeSslSocket(context)
with mock.patch.object(
auth_manager,
"reload_server_ssl_context",
wraps=auth_manager.reload_server_ssl_context,
) as reload_mock:
context.sni_callback(ssl_socket, "proxmenux.test", context)
reload_mock.assert_not_called()
self.assertIs(ssl_socket.context, context)
def test_valid_renewed_pair_is_activated_during_the_handshake(self):
context = self._create_context()
previous_fingerprint = auth_manager._SSL_RUNTIME_FINGERPRINT
self._install_pair(1)
ssl_socket = _FakeSslSocket(context)
context.sni_callback(ssl_socket, "proxmenux.test", context)
self.assertNotEqual(previous_fingerprint, auth_manager._SSL_RUNTIME_FINGERPRINT)
self.assertIs(ssl_socket.context, auth_manager._SSL_RUNTIME_CONTEXT)
self.assertIsNot(ssl_socket.context, context)
self.assertEqual(auth_manager._SSL_RUNTIME_LAST_REFRESH_ERROR, "")
def test_mismatched_pair_keeps_the_previous_context(self):
context = self._create_context()
previous_fingerprint = auth_manager._SSL_RUNTIME_FINGERPRINT
shutil.copyfile(self.pairs[1][0], self.active_cert)
ssl_socket = _FakeSslSocket(context)
context.sni_callback(ssl_socket, "proxmenux.test", context)
self.assertEqual(previous_fingerprint, auth_manager._SSL_RUNTIME_FINGERPRINT)
self.assertIs(auth_manager._SSL_RUNTIME_CONTEXT, context)
self.assertIs(ssl_socket.context, context)
self.assertTrue(auth_manager._SSL_RUNTIME_LAST_REFRESH_ERROR)
def test_custom_certificate_source_is_not_examined_automatically(self):
self._write_config("custom")
context = self._create_context()
previous_fingerprint = auth_manager._SSL_RUNTIME_FINGERPRINT
self._install_pair(1)
ssl_socket = _FakeSslSocket(context)
context.sni_callback(ssl_socket, "proxmenux.test", context)
self.assertEqual(previous_fingerprint, auth_manager._SSL_RUNTIME_FINGERPRINT)
self.assertIs(ssl_socket.context, context)
def test_first_real_tls_connection_receives_the_renewed_certificate(self):
server_context = self._create_context()
self._install_pair(1)
server_socket, client_socket = socket.socketpair()
server_socket.settimeout(5)
client_socket.settimeout(5)
server_error = []
def serve_once():
try:
with server_context.wrap_socket(server_socket, server_side=True) as tls_socket:
tls_socket.recv(1)
except Exception as error: # pragma: no cover - asserted below
server_error.append(error)
thread = threading.Thread(target=serve_once)
thread.start()
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
with client_context.wrap_socket(
client_socket, server_hostname="proxmenux.test"
) as tls_client:
received_der = tls_client.getpeercert(binary_form=True)
tls_client.sendall(b"x")
thread.join(timeout=5)
self.assertFalse(thread.is_alive())
self.assertEqual(server_error, [])
renewed_pem = self.pairs[1][0].read_text()
self.assertEqual(received_der, ssl.PEM_cert_to_DER_cert(renewed_pem))
def test_tls_connection_without_sni_also_receives_the_renewed_certificate(self):
server_context = self._create_context()
self._install_pair(1)
server_socket, client_socket = socket.socketpair()
server_socket.settimeout(5)
client_socket.settimeout(5)
server_error = []
def serve_once():
try:
with server_context.wrap_socket(server_socket, server_side=True) as tls_socket:
tls_socket.recv(1)
except Exception as error: # pragma: no cover - asserted below
server_error.append(error)
thread = threading.Thread(target=serve_once)
thread.start()
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
with client_context.wrap_socket(client_socket) as tls_client:
received_der = tls_client.getpeercert(binary_form=True)
tls_client.sendall(b"x")
thread.join(timeout=5)
self.assertFalse(thread.is_alive())
self.assertEqual(server_error, [])
renewed_pem = self.pairs[1][0].read_text()
self.assertEqual(received_der, ssl.PEM_cert_to_DER_cert(renewed_pem))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,69 @@
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
import lxc_apps # noqa: E402
class ScheduledUpdateRecordTests(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.temp_dir.cleanup)
self.apps_dir_patch = mock.patch.object(
lxc_apps, '_APPS_DIR', self.temp_dir.name,
)
self.apps_dir_patch.start()
self.addCleanup(self.apps_dir_patch.stop)
self.vmid = 9911
self.assertTrue(lxc_apps._write_sidecar(self.vmid, {
'vmid': self.vmid,
'apps': [],
'schedule': {'enabled': True, 'cron': '0 3 * * *'},
}))
def test_record_keeps_log_and_reboot_evidence(self):
self.assertTrue(lxc_apps.record_schedule_run(
self.vmid,
'success',
'both',
log_name='../9911-scheduled-' + ('a' * 32) + '.log',
reboot_required=True,
reboot_packages=['linux-image-amd64', 'libc6'],
))
schedule = lxc_apps._read_sidecar(self.vmid)['schedule']
self.assertEqual(
schedule['last_run_log'],
'9911-scheduled-' + ('a' * 32) + '.log',
)
self.assertTrue(schedule['last_run_reboot_required'])
self.assertEqual(
schedule['last_run_reboot_packages'],
['linux-image-amd64', 'libc6'],
)
def test_lifecycle_clear_preserves_run_and_log(self):
lxc_apps.record_schedule_run(
self.vmid,
'success',
'os',
log_name='9911-scheduled-' + ('b' * 32) + '.log',
reboot_required=True,
reboot_packages=['linux-image-amd64'],
)
self.assertTrue(lxc_apps.clear_schedule_reboot_required(self.vmid))
schedule = lxc_apps._read_sidecar(self.vmid)['schedule']
self.assertFalse(schedule['last_run_reboot_required'])
self.assertNotIn('last_run_reboot_packages', schedule)
self.assertEqual(schedule['last_run_status'], 'success')
self.assertIn('last_run_log', schedule)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,160 @@
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from unittest import mock
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
import notification_manager # noqa: E402
from notification_events import NotificationEvent # noqa: E402
class RecordingChannel:
def __init__(self, results=None):
self.results = list(results or [True])
self.calls = 0
self.lock = threading.Lock()
def send(self, title, body, severity, data):
with self.lock:
self.calls += 1
success = self.results.pop(0) if self.results else True
time.sleep(0.1)
return {'success': success, 'error': '' if success else 'temporary failure'}
class NotificationDeliveryDedupTests(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.temp_dir.cleanup)
self.db_path = Path(self.temp_dir.name) / 'health_monitor.db'
conn = sqlite3.connect(str(self.db_path))
conn.execute('''
CREATE TABLE notification_last_sent (
fingerprint TEXT PRIMARY KEY,
last_sent_ts INTEGER NOT NULL,
count INTEGER DEFAULT 1
)
''')
conn.execute('''
CREATE TABLE notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
channel TEXT NOT NULL,
title TEXT,
message TEXT,
severity TEXT,
sent_at TEXT NOT NULL,
success INTEGER DEFAULT 1,
error_message TEXT,
source TEXT DEFAULT 'server'
)
''')
conn.commit()
conn.close()
self.db_patch = mock.patch.object(
notification_manager, 'DB_PATH', self.db_path,
)
self.db_patch.start()
self.addCleanup(self.db_patch.stop)
self.ai_context_patch = mock.patch.object(
notification_manager, 'enrich_context_for_ai', return_value='',
)
self.ai_context_patch.start()
self.addCleanup(self.ai_context_patch.stop)
self.ai_rewrite_patch = mock.patch.object(
notification_manager, '_format_with_ai_bounded', return_value=None,
)
self.ai_rewrite_patch.start()
self.addCleanup(self.ai_rewrite_patch.stop)
def _manager(self, channel):
manager = notification_manager.NotificationManager()
manager._enabled = True
manager._config = {
'email.enabled': 'true',
'email.rich_format': 'false',
'ai_enabled': 'false',
}
manager._channels = {'email': channel}
return manager
@staticmethod
def _event():
return NotificationEvent(
event_type='lxc_update_applied',
severity='INFO',
data={
'hostname': 'pve-test',
'vmid': 210,
'ct_name': 'docker-frontend',
'target': 'Docker Engine',
'result': 'succeeded',
'duration': '10s',
'details': 'Update completed',
},
source='manual',
entity='ct',
entity_id='210:same-run',
)
def test_concurrent_managers_deliver_same_fingerprint_once(self):
channel = RecordingChannel()
managers = [self._manager(channel), self._manager(channel)]
barrier = threading.Barrier(3)
def dispatch(manager):
barrier.wait()
manager._dispatch_event(self._event())
threads = [
threading.Thread(target=dispatch, args=(manager,))
for manager in managers
]
for thread in threads:
thread.start()
barrier.wait()
for thread in threads:
thread.join(timeout=5)
self.assertEqual(channel.calls, 1)
conn = sqlite3.connect(str(self.db_path))
history_count = conn.execute(
'SELECT COUNT(*) FROM notification_history WHERE success = 1'
).fetchone()[0]
claim_count = conn.execute(
'SELECT COUNT(*) FROM notification_delivery_claims'
).fetchone()[0]
conn.close()
self.assertEqual(history_count, 1)
self.assertEqual(claim_count, 0)
def test_failed_delivery_releases_claim_for_retry(self):
channel = RecordingChannel([False, True])
manager = self._manager(channel)
manager._dispatch_event(self._event())
manager._dispatch_event(self._event())
self.assertEqual(channel.calls, 2)
conn = sqlite3.connect(str(self.db_path))
successes = conn.execute(
'SELECT success FROM notification_history ORDER BY id'
).fetchall()
claim_count = conn.execute(
'SELECT COUNT(*) FROM notification_delivery_claims'
).fetchone()[0]
conn.close()
self.assertEqual(successes, [(0,), (1,)])
self.assertEqual(claim_count, 0)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,114 @@
import json
import sys
import tempfile
import unittest
from pathlib import Path
from queue import Queue
from unittest import mock
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
import notification_events # noqa: E402
import notification_templates # noqa: E402
import post_install_versions # noqa: E402
class NotificationUpdatePolicyTests(unittest.TestCase):
def test_apt_listchanges_system_mail_has_its_own_update_event(self):
watcher = notification_events.ProxmoxHookWatcher(Queue())
classified = watcher._classify_pve(
"system-mail",
"info",
"Novedades de apt-listchanges para amd",
"zfs-linux recommends that all users update absolute paths",
)
self.assertEqual(classified, ("apt_listchanges", "node", ""))
def test_regular_system_mail_remains_available(self):
watcher = notification_events.ProxmoxHookWatcher(Queue())
with mock.patch.object(notification_events, "_record_smartd_observation_impl"):
classified = watcher._classify_pve(
"system-mail",
"warning",
"SMART error (CurrentPendingSector) detected on host",
"Device: /dev/sda",
)
self.assertEqual(classified, ("system_mail", "node", ""))
def test_apt_listchanges_body_is_preserved_and_attributed(self):
watcher = notification_events.ProxmoxHookWatcher(Queue())
upstream = (
"zfs-linux (2.2.4-2) unstable; urgency=medium\n\n"
" Package-maintainer recommendation.\n\n"
" -- Maintainer <maintainer@example.com>"
)
result = watcher.process_webhook({
"title": "apt-listchanges: News for host",
"message": upstream,
"severity": "info",
"fields": {"type": "system-mail", "hostname": "pve-test"},
})
self.assertTrue(result["accepted"])
event = watcher._queue.get_nowait()
self.assertEqual(event.event_type, "apt_listchanges")
self.assertEqual(event.data["reason"], upstream)
rendered = notification_templates.render_template(
event.event_type,
event.data,
)
self.assertIn("not a ProxMenux recommendation", rendered["body_text"])
self.assertIn(upstream, rendered["body_text"])
def test_smaller_pending_subset_does_not_notify_again(self):
updates = [
{"key": "persistent_network", "available_version": "1.2"},
]
notified = {
"log2ram": {"1.4"},
"persistent_network": {"1.2"},
}
self.assertEqual(
notification_events._new_post_install_update_versions(updates, notified),
{},
)
def test_new_version_of_existing_tool_is_detected(self):
updates = [
{"key": "persistent_network", "available_version": "1.3"},
]
notified = {"persistent_network": {"1.2"}}
self.assertEqual(
notification_events._new_post_install_update_versions(updates, notified),
{"persistent_network": "1.3"},
)
def test_notified_versions_share_the_existing_snapshot_file(self):
with tempfile.TemporaryDirectory() as temporary:
snapshot_path = Path(temporary) / "updates_available.json"
cache = {
"scanned_at": 123.0,
"updates": [
{"key": "log2ram", "available_version": "1.4"},
],
}
with mock.patch.object(post_install_versions, "_UPDATES_JSON", snapshot_path), \
mock.patch.object(post_install_versions, "_cache", cache):
post_install_versions.save_notified_versions(
{"log2ram": {"1.3", "1.4"}}
)
self.assertEqual(
post_install_versions.load_notified_versions(),
{"log2ram": {"1.3", "1.4"}},
)
payload = json.loads(snapshot_path.read_text(encoding="utf-8"))
self.assertEqual(payload["scanned_at"], 123.0)
self.assertEqual(payload["updates"], cache["updates"])
self.assertEqual(payload["notified_versions"]["log2ram"], ["1.3", "1.4"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,37 @@
import sys
from pathlib import Path
import unittest
from unittest.mock import MagicMock, patch
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
import lxc_apps
class AdguardSetupTests(unittest.TestCase):
def test_non_adguard_is_not_probed(self):
with patch.object(lxc_apps, '_oci_instance_meta', return_value={'template_id': 'image-frigate'}), \
patch.object(lxc_apps.subprocess, 'run') as run:
self.assertFalse(lxc_apps.oci_adguard_setup_available(190))
run.assert_not_called()
def test_setup_returns_true_only_for_http_response(self):
process = MagicMock(stdout='192.168.0.42\n')
connection = MagicMock()
connection.__enter__.return_value.recv.return_value = b'HTTP/1.1 302 Found\r\n'
with patch.object(lxc_apps, '_oci_instance_meta', return_value={'template_id': 'image-adguard-home'}), \
patch.object(lxc_apps.subprocess, 'run', return_value=process), \
patch.object(lxc_apps.socket, 'create_connection', return_value=connection) as connect:
self.assertTrue(lxc_apps.oci_adguard_setup_available(190))
connect.assert_called_once_with(('192.168.0.42', 3000), timeout=1)
def test_closed_setup_port_is_not_offered(self):
process = MagicMock(stdout='192.168.0.42\n')
with patch.object(lxc_apps, '_oci_instance_meta', return_value={'template_id': 'image-adguard-home'}), \
patch.object(lxc_apps.subprocess, 'run', return_value=process), \
patch.object(lxc_apps.socket, 'create_connection', side_effect=OSError):
self.assertFalse(lxc_apps.oci_adguard_setup_available(190))
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Regression tests based on LeidenSpain's real LXC OOM block."""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from proxmox_known_errors import ( # noqa: E402
analyze_oom_event,
format_oom_diagnosis,
get_error_context,
)
LXC_OOM = """
apt-get invoked oom-killer: gfp_mask=0x101cca, order=3, oom_score_adj=0
memory: usage 131056kB, limit 131072kB, failcnt 1922
swap: usage 0kB, limit 0kB, failcnt 0
Memory cgroup stats for /lxc/108:
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=ns,mems_allowed=0,oom_memcg=/lxc/108,task_memcg=/lxc/108/ns/.lxc,task=apt-get,pid=1183877,uid=100000
Memory cgroup out of memory: Killed process 1183877 (apt-get) total-vm:79700kB, anon-rss:65056kB
"""
class OomDiagnosticsTest(unittest.TestCase):
def test_lxc_memcg_scope_and_limits(self):
result = analyze_oom_event(LXC_OOM)
self.assertIsNotNone(result)
self.assertEqual(result['scope'], 'lxc')
self.assertEqual(result['ctid'], '108')
self.assertEqual(result['constraint'], 'CONSTRAINT_MEMCG')
self.assertEqual(result['memory_usage_kib'], 131056)
self.assertEqual(result['memory_limit_kib'], 131072)
self.assertEqual(result['swap_limit_kib'], 0)
self.assertEqual(result['victim_process'], 'apt-get')
self.assertEqual(result['victim_pid'], '1183877')
def test_diagnosis_does_not_blame_host(self):
diagnosis = format_oom_diagnosis(analyze_oom_event(LXC_OOM))
self.assertIn('LXC 108', diagnosis)
self.assertIn('not a host-wide OOM', diagnosis)
self.assertIn('128.0 MiB used of 128.0 MiB', diagnosis)
self.assertIn('Killed process: apt-get', diagnosis)
def test_known_error_context_includes_event_evidence(self):
context = get_error_context(LXC_OOM, category='memory', detail_level='detailed')
self.assertIn('Event analysis:', context)
self.assertIn('LXC 108 memory cgroup', context)
if __name__ == '__main__':
unittest.main()