overhaul app tracking and update orchestration

- Generate and ship a verified 389-app tracking catalog with 23 runtime overrides, fallback detectors, ports, logos, and Docker Hub tag previews.
- Support modern Proxmox VE Helper-Scripts markers, historical installations, and official or manual app deployments.
- Rework the LXC App and Updates tabs with cached suggestions, explicit discovery, version tracking, web links, custom updaters, and complete i18n.
- Add independent OS, app, Docker Engine, Docker image, bulk, and scheduled update targets.
- Add digest-based Docker inventory, Compose dependency grouping, safe standalone-container recreation with rollback, and package-scoped Docker Engine updates.
- Refresh per-LXC caches after lifecycle and update tasks, then emit idempotent notifications based on the verified final state.
- Harden Coral USB recovery by removing orphaned gasket DKMS registrations and validating that dpkg is healthy before reporting success.
This commit is contained in:
MacRimi
2026-08-23 12:43:03 +02:00
parent 7244201810
commit 0251f77331
27 changed files with 11631 additions and 893 deletions
+518 -81
View File
@@ -10,10 +10,9 @@
* • an optional GitHub repo for upstream version tracking
* • a list of ports, each with a description and web path
*
* Docker apps are "register-only": they exist to produce clickable
* links, ProxMenux does NOT try to track their version and NEVER
* emits warnings for them — updates for Docker apps are handled by
* Docker itself.
* Docker image updates live exclusively in the Updates tab. The App
* tab only registers the Docker engine/app identity and shows installed
* metadata, so an unregistered detection can never create update noise.
*
* For ProxMenux-managed OCI CTs (Secure Gateway) the panel is
* read-only — the actual update lifecycle lives in Security →
@@ -24,8 +23,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"
import {
Loader2, Save, RefreshCw, Trash2, Package, ExternalLink,
AlertTriangle, Info, PlusCircle, Pencil, ChevronDown, ChevronRight, EyeOff,
ArrowUpCircle, RotateCcw, Check, Settings2, ShieldCheck, CheckCircle2,
Bell, BellOff,
ArrowUpCircle, RotateCcw, Check, Settings2, ShieldCheck,
Bell, BellOff, Search,
} from "lucide-react"
import { Card, CardContent } from "./ui/card"
import { Button } from "./ui/button"
@@ -34,7 +33,7 @@ import { Label } from "./ui/label"
import { Badge } from "./ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { fetchLxcApps, getLxcAppsCached, invalidateLxcApps } from "../lib/lxc-apps-cache"
import { fetchLxcApps, getLxcAppsCached, setLxcAppsCached } from "../lib/lxc-apps-cache"
import { useT } from "@/lib/i18n/provider"
// installed_via is optional now — an empty value means "register only,
@@ -84,6 +83,11 @@ interface AppConfig {
health_path?: string
logo_url?: string
helper_slug?: string
// Preserved here even though this editor does not execute updates.
// The backend uses full-record replacement, so omitting these when
// editing ports/tracking would silently erase the Updates-tab setup.
update_command?: string
hide_no_updater_notice?: boolean
// Per-app opt-out for the `app_update_available` notification.
// Absent / true = notify; false = silenced. Set from the bell
// toggle on each app card and/or the Edit form's checkbox.
@@ -104,6 +108,7 @@ interface DetectedApp {
interface AppState {
installed_version: string | null
latest_version: string | null
latest_published_at?: string | null
update_available: boolean | null
error: string | null
checked_at: string | null
@@ -123,18 +128,77 @@ interface SidecarResponse {
updated_at?: string
}
interface DetectorTestResult {
valid: boolean
persisted: false
checked_at: string
installed: {
configured: boolean
method: InstalledVia | null
effective_regex: string | null
version: string | null
error: string | null
}
upstream: {
configured: boolean
type: "github" | "http_json" | "docker_hub" | null
version: string | null
published_at?: string | null
error: string | null
}
update_available: boolean | null
}
interface TrackingSuggestion {
installed_via: "dpkg" | "apk" | "file" | "binary"
installed_via: Exclude<InstalledVia, "">
package?: string
file_path?: string
file_regex?: string
binary_path?: string
binary_args?: string[]
python_path?: string
distribution?: string
container_name?: string
label?: string
command_argv?: string[]
installed_version?: string
installed_regex?: string
upstream_type?: "github" | "http_json" | "docker_hub" | ""
upstream_url?: string
upstream_json_path?: string
docker_image?: string
repo?: string
github_source?: "releases" | "tags"
tag_regex?: string
detected_version?: string
detector_verified?: boolean
detector_source?: "primary" | "alternative" | "helper_marker" | "legacy_fallback" | "runtime_probe" | "candidate"
detector_error?: string
}
interface DockerTagPreview {
image: string
regex: string
tags: Array<{ tag: string; version: string | null; moving: boolean }>
matched_count: number
scanned_count: number
cached_for_seconds: number
}
interface DockerWebLinkSuggestion {
container_name: string
service_name: string
service_slug?: string | null
image: string
host_port: number
container_port: number
scheme: "http" | "https"
web_path: string
logo_url?: string | null
}
interface Suggestions {
ready?: boolean
name_suggestion: string | null
helper_slug: string | null
port_suggestions: number[]
@@ -143,6 +207,7 @@ interface Suggestions {
default_ports?: number[]
logo_url?: string | null
extras?: DetectedApp[]
docker_web_links?: DockerWebLinkSuggestion[]
}
// Compact catalog entry — one row for every registerable app the
@@ -209,6 +274,44 @@ const EMPTY_APP: AppConfig = {
logo_url: "",
}
const SELFHST_WEBP_BASE = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp"
const SELFHST_THEME_LOGOS: Record<string, { lightTheme: string; darkTheme: string }> = {
frigate: {
lightTheme: `${SELFHST_WEBP_BASE}/frigate-dark.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/frigate-light.webp`,
},
portainer: {
lightTheme: `${SELFHST_WEBP_BASE}/portainer-dark.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/portainer-light.webp`,
},
vaultwarden: {
lightTheme: `${SELFHST_WEBP_BASE}/vaultwarden.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/vaultwarden-light.webp`,
},
}
function selfhstThemeLogos(src: string) {
if (!src.toLowerCase().includes("cdn.jsdelivr.net/gh/selfhst/icons")) return null
const match = src.toLowerCase().match(/\/(frigate|portainer|vaultwarden)(?:-(?:dark|light))?\.webp(?:[?#].*)?$/)
return match ? SELFHST_THEME_LOGOS[match[1]] : null
}
export function ThemeAwareLogo({ src, className }: { src: string; className: string }) {
const themed = selfhstThemeLogos(src)
const hideBroken = (e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.style.display = "none"
}
if (!themed) {
return <img src={src} alt="" className={className} onError={hideBroken} />
}
return (
<>
<img src={themed.lightTheme} alt="" className={`${className} block dark:hidden`} onError={hideBroken} />
<img src={themed.darkTheme} alt="" className={`${className} hidden dark:block`} onError={hideBroken} />
</>
)
}
// Default scheme heuristic for freshly-added ports — only used to
// pre-select the dropdown. The user always has the final say via
// the http/https selector next to the port input.
@@ -245,9 +348,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const [sidecar, setSidecar] = useState<SidecarResponse | null>(seed?.sidecar ?? null)
const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null)
const [error, setError] = useState<string | null>(null)
const [searchingApplications, setSearchingApplications] = useState(false)
const [detectionNotice, setDetectionNotice] = useState<{ found: boolean; text: string } | null>(null)
// Editor state
const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null)
const [saving, setSaving] = useState(false)
const [testingDetector, setTestingDetector] = useState(false)
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
const [busyAppId, setBusyAppId] = useState<string | null>(null)
// Advanced section (version tracking) is collapsed by default so the
// basic Name + Ports flow stays approachable. Auto-expanded when
@@ -270,11 +377,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
// restore, this panel is skipped entirely and the button opens the
// editor directly (fast path for the common case).
const [browseOpen, setBrowseOpen] = useState(false)
const [dockerTagPreview, setDockerTagPreview] = useState<DockerTagPreview | null>(null)
const [dockerTagPreviewLoading, setDockerTagPreviewLoading] = useState(false)
const [dockerTagPreviewError, setDockerTagPreviewError] = useState<string | null>(null)
// Global "manage apps" mode. When ON, every app card grows a footer
// with Remove / Check / Edit fields actions. When OFF the cards are
// pure info — the only surfaced action is a hover-reveal Check icon
// on the LATEST UPSTREAM panel. Toggled from a single button next to
// pure info; detector checks remain available after enabling Edit.
// Toggled from a single button next to
// "Add another application".
const [editMode, setEditMode] = useState(false)
@@ -309,6 +419,42 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
useEffect(() => { load() }, [load])
// Live Docker Hub preview. Debounced so typing an image/regex does not
// issue one request per keystroke; the backend additionally caches the
// raw tag list for 60 seconds per image.
useEffect(() => {
const draft = editing?.draft
const image = (draft?.docker_image || "").trim()
if (draft?.upstream_type !== "docker_hub" || !image) {
setDockerTagPreview(null)
setDockerTagPreviewError(null)
setDockerTagPreviewLoading(false)
return
}
let cancelled = false
setDockerTagPreview(null)
setDockerTagPreviewError(null)
const timer = window.setTimeout(async () => {
setDockerTagPreviewLoading(true)
try {
const result: DockerTagPreview = await fetchApi("/api/lxc-apps/dockerhub-tag-preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image, regex: draft.tag_regex || "" }),
})
if (!cancelled) setDockerTagPreview(result)
} catch (e: any) {
if (!cancelled) setDockerTagPreviewError(e?.message || t("vmLxc.appEditor.dockerTagPreviewFailed"))
} finally {
if (!cancelled) setDockerTagPreviewLoading(false)
}
}, 500)
return () => {
cancelled = true
window.clearTimeout(timer)
}
}, [editing?.draft.docker_image, editing?.draft.tag_regex, editing?.draft.upstream_type, t])
// Turn a raw backend error string ("network error: The read operation
// timed out", etc.) into a localized message. Upstream check errors
// are surfaced verbatim by `lxc_apps.py:_fetch_upstream()`, and the
@@ -329,18 +475,19 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
return msg
}
// Fetch the picker catalog once per panel mount. Best-effort — if
// the API is unreachable, the picker just stays empty and users
// type the app name manually (same as before this feature).
const editorOpen = !!editing
// The picker catalog is only needed after the user opens the editor.
useEffect(() => {
if (!editorOpen || catalog.length > 0) return
let cancelled = false
fetchApi("/api/apps/catalog")
fetchApi<CatalogEntry[]>("/api/apps/catalog")
.then((data: CatalogEntry[]) => {
if (!cancelled && Array.isArray(data)) setCatalog(data)
})
.catch(() => { /* non-fatal */ })
return () => { cancelled = true }
}, [])
}, [editorOpen, catalog.length])
// Derived state — computed here BEFORE any conditional early
// return so React sees the same hook order on every render.
@@ -396,6 +543,47 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
// different-app panel. Not affected by registration state.
const hiddenDetections = detectedList.filter((d) => dismissedSlugs.has(d.slug))
const searchInstalledApplications = async () => {
const before = new Set(visibleDetected.map((item) => item.slug))
setSearchingApplications(true)
setDetectionNotice(null)
setError(null)
try {
const result: Suggestions = await fetchApi(`/api/vms/${vmid}/apps/suggestions`, {
method: "POST",
})
setSuggestions(result)
if (sidecar) setLxcAppsCached(vmid, sidecar, result)
const detected = new Set<string>()
if (result.helper_slug) detected.add(result.helper_slug)
for (const item of result.extras || []) detected.add(item.slug)
const visible = [...detected].filter(
(slug) => !registeredSlugs.has(slug) && !dismissedSlugs.has(slug),
)
const newCount = visible.filter((slug) => !before.has(slug)).length
if (newCount === 1) {
setDetectionNotice({ found: true, text: t("vmLxc.appEditor.oneNewApplicationDetected") })
} else if (newCount > 1) {
setDetectionNotice({
found: true,
text: t("vmLxc.appEditor.newApplicationsDetected", { count: newCount }),
})
} else {
setDetectionNotice({
found: false,
text: t(visible.length === 0 && apps.length === 0
? "vmLxc.appEditor.noApplicationsDetected"
: "vmLxc.appEditor.noNewApplicationsDetected"),
})
}
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.detectionFailed"))
} finally {
setSearchingApplications(false)
}
}
// "Register a different app" behavior: if there are hidden slugs,
// surface them first (with Restore) so the user can bring one back
// instead of typing everything by hand. If nothing to restore, go
@@ -437,6 +625,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
health_path: existing.health_path || "",
logo_url: existing.logo_url || "",
helper_slug: existing.helper_slug || "",
update_command: existing.update_command || "",
hide_no_updater_notice: existing.hide_no_updater_notice === true,
notifications_enabled: existing.notifications_enabled !== false,
exclude_from_badge: existing.exclude_from_badge === true,
}
@@ -461,7 +651,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
seed.name = p.name
seed.logo_url = p.logo_url || ""
seed.helper_slug = p.slug
if (p.default_ports?.length) {
// 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) => ({
port,
scheme: defaultSchemeFor(port),
@@ -477,6 +670,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
file_path: t.file_path || "",
file_regex: t.file_regex || "",
binary_path: t.binary_path || "",
binary_args: t.binary_args ? [...t.binary_args] : [],
python_path: t.python_path || "",
distribution: t.distribution || "",
container_name: t.container_name || "",
label: t.label || "",
command_argv: t.command_argv ? [...t.command_argv] : [],
installed_version: t.installed_version || "",
installed_regex: t.installed_regex || "",
upstream_type: (t as any).upstream_type || (t.repo ? "github" : ""),
repo: t.repo || "",
github_source: t.github_source || "releases",
@@ -497,11 +698,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
}
}
setEditing({ appId: existing?.id || null, draft: seed })
setDetectorTest(null)
setError(null)
}, [suggestions, vmid, sidecar])
const closeEditor = () => {
setEditing(null)
setDetectorTest(null)
setError(null)
}
@@ -521,7 +724,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
})
if ((r as any).error) throw new Error((r as any).error)
setSidecar(r)
invalidateLxcApps(vmid)
setLxcAppsCached(vmid, r, suggestions)
setEditing(null)
onChange?.()
} catch (e: any) {
@@ -531,6 +734,25 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
}
}
const testDetector = async () => {
if (!editing?.draft.installed_via) return
setTestingDetector(true)
setDetectorTest(null)
setError(null)
try {
const result: DetectorTestResult = await fetchApi(`/api/vms/${vmid}/apps/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(editing.draft),
})
setDetectorTest(result)
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.detectorTestFailed"))
} finally {
setTestingDetector(false)
}
}
const checkOne = async (appId: string) => {
setBusyAppId(appId)
setError(null)
@@ -539,7 +761,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
method: "POST",
})
setSidecar(r)
invalidateLxcApps(vmid)
setLxcAppsCached(vmid, r, suggestions)
onChange?.()
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.checkFailed"))
@@ -565,7 +787,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
body: JSON.stringify(payload),
})
setSidecar(r)
invalidateLxcApps(vmid)
setLxcAppsCached(vmid, r, suggestions)
onChange?.()
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.saveFailed"))
@@ -579,10 +801,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
setBusyAppId(appId)
setError(null)
try {
await fetchApi(`/api/vms/${vmid}/apps/${appId}`, { method: "DELETE" })
// Reload from server so the empty state re-fetches suggestions
invalidateLxcApps(vmid)
await load()
const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps/${appId}`, { method: "DELETE" })
setSidecar(r)
setLxcAppsCached(vmid, r, suggestions)
onChange?.()
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.deleteFailed"))
@@ -608,7 +829,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
body: JSON.stringify({ slug, dismissed: true }),
})
setSidecar(r)
invalidateLxcApps(vmid)
setLxcAppsCached(vmid, r, suggestions)
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.dismissFailed"))
await load() // resync on failure
@@ -631,7 +852,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
body: JSON.stringify({ slug, dismissed: false }),
})
setSidecar(r)
invalidateLxcApps(vmid)
setLxcAppsCached(vmid, r, suggestions)
} catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.restoreFailed"))
await load()
@@ -666,7 +887,6 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const repo = isSecureGateway ? "tailscale/tailscale" : ""
const methodLine = isSecureGateway ? `apk · tailscale · ${t("vmLxc.appEditor.managedStatus")}` : t("vmLxc.appEditor.managedStatus")
const hasUpdate = managed.update_available === true
const upToDate = managed.update_available === false && !!managed.installed_version
const showVersions = !!(managed.installed_version || managed.latest_version || repo)
return (
@@ -756,16 +976,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
)}
{showVersions && (
<div className={"mt-3 grid gap-3 " + (repo ? "grid-cols-2" : "grid-cols-1")}>
{showVersions && (managed.installed_version || repo) && (
<div className={"mt-3 grid gap-3 " + (managed.installed_version && repo ? "grid-cols-2" : "grid-cols-1")}>
{managed.installed_version && (
<div className="p-3 rounded-md bg-muted/40">
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.installedStatus")}</div>
<div className="text-lg font-semibold font-mono text-foreground flex items-center gap-2">
<div className="text-lg font-semibold font-mono text-foreground">
{managed.installed_version}
{upToDate && (
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
)}
</div>
</div>
)}
@@ -818,8 +1035,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const draft = editing.draft
const method = draft.installed_via || ""
const isPackaged = method === "dpkg" || method === "apk"
const setField = (patch: Partial<AppConfig>) =>
const setField = (patch: Partial<AppConfig>) => {
setDetectorTest(null)
setEditing({ ...editing, draft: { ...draft, ...patch } })
}
// Editing the Name auto-fills the Package field on packaged
// methods when it's still empty. Rationale: 90% of the time the
// dpkg/apk package name mirrors the friendly app name (jellyfin,
@@ -853,10 +1072,37 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
setField({ ports: [...draft.ports, { port, description: "", scheme }] })
}
}
const addDockerWebLink = (link: DockerWebLinkSuggestion) => {
const entry: PortEntry = {
port: link.host_port,
description: link.service_name,
scheme: link.scheme,
web_path: link.web_path || "/",
logo_url: link.logo_url || "",
}
const last = draft.ports[draft.ports.length - 1]
if (last && last.port === "" && !last.description) {
const ports = [...draft.ports]
ports[ports.length - 1] = entry
setField({ ports })
} else {
setField({ ports: [...draft.ports, entry] })
}
}
const removePort = (i: number) =>
setField({ ports: draft.ports.filter((_, idx) => idx !== i) })
const usedPorts = new Set(draft.ports.map((p) => p.port))
const suggestable = (suggestions?.port_suggestions || []).filter((p) => !usedPorts.has(p))
const isDockerDraft = draft.helper_slug === "docker" ||
(draft.installed_via === "binary" && draft.binary_path?.endsWith("/docker"))
const suggestableDockerLinks = isDockerDraft
? (suggestions?.docker_web_links || []).filter((link) => !usedPorts.has(link.host_port))
: []
// A Docker registration uses structured container → published-port
// suggestions below. Suppress the generic ss/netstat chips in that case
// so the same endpoint is not presented twice without its workload name.
const suggestable = isDockerDraft
? []
: (suggestions?.port_suggestions || []).filter((p) => !usedPorts.has(p))
return (
<div className="space-y-4">
@@ -908,13 +1154,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
onClick={async () => {
setPickerOpen(false)
try {
const detail: CatalogDetail = await fetchApi(`/api/apps/catalog/${c.slug}`)
const detail: CatalogDetail = await fetchApi(`/api/apps/catalog/${c.slug}?vmid=${vmid}`)
// Seed the entire form from the picker detail.
const patch: Partial<AppConfig> = {
name: detail.name,
helper_slug: detail.slug,
logo_url: detail.logo_url || "",
ports: detail.default_ports?.length
ports: detail.slug !== "docker" && detail.default_ports?.length
? detail.default_ports.map((p) => ({
port: p,
scheme: defaultSchemeFor(p),
@@ -1002,6 +1248,53 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</Button>
</div>
{suggestableDockerLinks.length > 0 && (
<div className="mb-3 space-y-2">
<div>
<div className="text-xs font-medium text-foreground">
{t("vmLxc.appEditor.dockerPublishedServicesTitle")}
</div>
<p className="text-[10px] text-muted-foreground mt-0.5 leading-relaxed max-w-2xl">
{t("vmLxc.appEditor.dockerPublishedServicesHelp")}
</p>
</div>
<div className="divide-y divide-border/50 border-y border-border/50">
{suggestableDockerLinks.map((link) => (
<div
key={`${link.container_name}:${link.host_port}`}
className="flex flex-col sm:flex-row sm:items-center gap-2 py-2"
>
{link.logo_url && (
<ThemeAwareLogo
src={link.logo_url}
className="h-7 w-7 rounded object-contain flex-shrink-0"
/>
)}
<div className="min-w-0 flex-1">
<div className="text-sm text-foreground truncate">{link.service_name}</div>
<div className="text-[10px] text-muted-foreground truncate" title={link.image}>
{link.image} · {t("vmLxc.appEditor.dockerPublishedPort", {
containerPort: link.container_port,
hostPort: link.host_port,
})}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addDockerWebLink(link)}
className="h-8 flex-shrink-0"
>
<PlusCircle className="h-3.5 w-3.5 mr-1" />
{t("vmLxc.appEditor.dockerPublishedAdd")}
</Button>
</div>
))}
</div>
</div>
)}
{/* Detected chips FIRST — one-click add. Only shown when
there are chips left to suggest, so empty states stay
clean. Click on a chip: fills the current empty row
@@ -1024,7 +1317,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
)}
{draft.ports.length === 0 && suggestable.length === 0 && (
{draft.ports.length === 0 && suggestable.length === 0 && suggestableDockerLinks.length === 0 && (
<div className="text-xs text-muted-foreground italic">
{t("vmLxc.appEditor.noWebPorts")}
</div>
@@ -1470,9 +1763,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
maxLength={255}
/>
<div className="text-[10px] text-muted-foreground mt-1">
<code className="text-foreground/70">owner/name</code> — or bare
name for official images. ProxMenux picks the highest semver tag
matching the filter below.
{t("vmLxc.appEditor.dockerVersionedTagsHelp")}
</div>
</div>
)}
@@ -1489,13 +1780,82 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
placeholder={t("vmLxc.appEditor.tagRegexPlaceholder")}
className="font-mono text-xs"
/>
{upstreamType === "docker_hub" && (
<div className="flex flex-wrap gap-1.5 mt-2">
<Button
type="button"
variant="outline"
size="sm"
className="h-7 text-[10px]"
onClick={() => setField({ tag_regex: "^v?(\\d+\\.\\d+\\.\\d+)$" })}
>
{t("vmLxc.appEditor.dockerPresetSemver")}
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 text-[10px]"
onClick={() => setField({ tag_regex: "^v?(\\d+\\.\\d+\\.\\d+(?:[-+._][0-9A-Za-z.-]+)?)$" })}
>
{t("vmLxc.appEditor.dockerPresetSemverSuffix")}
</Button>
</div>
)}
<div className="text-[10px] text-muted-foreground mt-1">
{upstreamType === "github" && "Extracts the version from the release tag name."}
{upstreamType === "http_json" && "Optional — extract a substring from the endpoint's value."}
{upstreamType === "docker_hub" && "Filter which tags qualify (e.g. only semver). Applied before picking the highest."}
{upstreamType === "docker_hub" && t("vmLxc.appEditor.dockerTagFilterHelp")}
</div>
</div>
)}
{upstreamType === "docker_hub" && draft.docker_image?.trim() && (
<div className="rounded-md border border-border/70 bg-background/50 p-3">
<div className="flex items-center justify-between gap-2 mb-2">
<div className="text-xs font-medium text-foreground">
{t("vmLxc.appEditor.dockerTagPreviewLabel")}
</div>
{dockerTagPreview && (
<span className="text-[10px] text-muted-foreground">
{t("vmLxc.appEditor.dockerTagPreviewCount", {
matched: dockerTagPreview.matched_count,
scanned: dockerTagPreview.scanned_count,
})}
</span>
)}
</div>
{dockerTagPreviewLoading ? (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("vmLxc.appEditor.dockerTagPreviewLoading")}
</div>
) : dockerTagPreviewError ? (
<div className="text-xs text-amber-300">{dockerTagPreviewError}</div>
) : dockerTagPreview?.tags.length ? (
<div className="flex flex-wrap gap-1.5">
{dockerTagPreview.tags.map((entry) => (
<Badge
key={entry.tag}
variant="outline"
className={entry.moving ? "border-amber-500/40 text-amber-300" : "font-mono"}
>
{entry.tag}{entry.moving ? ` · ${t("vmLxc.appEditor.dockerMovingTag")}` : ""}
</Badge>
))}
</div>
) : (
<div className="text-xs text-muted-foreground">
{t("vmLxc.appEditor.dockerTagPreviewEmpty")}
</div>
)}
{dockerTagPreview?.tags.some((entry) => entry.moving) && (
<div className="mt-2 text-[10px] text-amber-300 leading-relaxed">
{t("vmLxc.appEditor.dockerMovingTagHelp")}
</div>
)}
</div>
)}
</>
)
})()}
@@ -1541,6 +1901,47 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
)}
{detectorTest && (
<div className="rounded-md border border-border/70 bg-background/60 p-3 space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-medium text-foreground">
{t("vmLxc.appEditor.detectorTestTitle")}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className={"rounded-md border p-2.5 " + (detectorTest.installed.version ? "border-emerald-500/30 bg-emerald-500/5" : "border-amber-500/30 bg-amber-500/5")}>
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">
{t("vmLxc.appEditor.installedStatus")}
</div>
{detectorTest.installed.version ? (
<div className="font-mono text-sm text-emerald-400">{detectorTest.installed.version}</div>
) : (
<div className="text-xs text-amber-300">{detectorTest.installed.error || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
)}
<div className="mt-1.5 text-[10px] text-muted-foreground break-all">
{detectorTest.installed.method || "—"}
{detectorTest.installed.effective_regex ? ` · ${detectorTest.installed.effective_regex}` : ""}
</div>
</div>
<div className={"rounded-md border p-2.5 " + (!detectorTest.upstream.configured || detectorTest.upstream.version ? "border-emerald-500/30 bg-emerald-500/5" : "border-amber-500/30 bg-amber-500/5")}>
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">
{t("vmLxc.appEditor.latestUpstream")}
</div>
{!detectorTest.upstream.configured ? (
<div className="text-xs text-muted-foreground">{t("vmLxc.appEditor.detectorTestNoUpstream")}</div>
) : detectorTest.upstream.version ? (
<div className="font-mono text-sm text-emerald-400">{detectorTest.upstream.version}</div>
) : (
<div className="text-xs text-amber-300">{detectorTest.upstream.error || t("vmLxc.appEditor.detectorTestNoVersion")}</div>
)}
{detectorTest.upstream.type && (
<div className="mt-1.5 text-[10px] text-muted-foreground">{detectorTest.upstream.type}</div>
)}
</div>
</div>
</div>
)}
{error && (
<div className="text-xs text-red-400 flex items-start gap-1.5">
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
@@ -1549,10 +1950,22 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)}
<div className="flex flex-wrap gap-2 justify-end pt-2">
<Button variant="ghost" onClick={closeEditor} disabled={saving}>{t("vmLxc.appEditor.cancelButton")}</Button>
{method && (
<Button
type="button"
variant="outline"
onClick={testDetector}
disabled={saving || testingDetector || !draft.name.trim()}
className="sm:mr-auto"
>
{testingDetector ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <ShieldCheck className="h-4 w-4 mr-1.5" />}
{testingDetector ? t("vmLxc.appEditor.testingDetectorButton") : t("vmLxc.appEditor.testDetectorButton")}
</Button>
)}
<Button variant="ghost" onClick={closeEditor} disabled={saving || testingDetector}>{t("vmLxc.appEditor.cancelButton")}</Button>
<Button
onClick={save}
disabled={saving || !draft.name.trim()}
disabled={saving || testingDetector || !draft.name.trim()}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
{saving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <Save className="h-4 w-4 mr-1.5" />}
@@ -1573,11 +1986,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
{d.logo_url && (
<img
<ThemeAwareLogo
src={d.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
/>
)}
<div className="min-w-0">
@@ -1613,16 +2024,23 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
{d.logo_url && (
<img
<ThemeAwareLogo
src={d.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
/>
)}
<div className="min-w-0">
<div className="text-sm font-semibold text-foreground truncate">{d.name}</div>
<div className="text-xs text-emerald-400/90">{t("vmLxc.appEditor.detectedInContainer")}</div>
<div className="text-xs text-emerald-400/90">
{d.tracking_suggestion?.detector_verified
? t("vmLxc.appEditor.versionDetected", { version: d.tracking_suggestion.detected_version || "" })
: t("vmLxc.appEditor.detectedInContainer")}
</div>
{d.tracking_suggestion?.detector_source === "legacy_fallback" && (
<div className="text-[10px] text-amber-400/90 mt-0.5">
{t("vmLxc.appEditor.legacyDetectorUsed")}
</div>
)}
</div>
</div>
<div className="flex flex-row gap-2 flex-shrink-0 sm:justify-end w-full sm:w-auto">
@@ -1716,11 +2134,25 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
{visibleDetected.map(renderDetectionChip)}
</div>
)}
<div className="pt-1 flex justify-center">
<Button onClick={openBrowseOrEditor} variant={visibleDetected.length > 0 ? "outline" : "default"}
className={visibleDetected.length > 0 ? "" : "bg-blue-500 hover:bg-blue-600 text-white"}>
<div className="pt-1 flex flex-wrap justify-center gap-2">
<Button
onClick={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")}
</Button>
<Button
onClick={openBrowseOrEditor}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
<PlusCircle className="h-4 w-4 mr-1.5" />
{visibleDetected.length > 0 ? t("vmLxc.appEditor.registerDifferent") : t("vmLxc.appEditor.registerApplication")}
{t("vmLxc.appEditor.registerApplication")}
{hiddenDetections.length > 0 && (
<span className="ml-2 text-[10px] opacity-70">
· {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })}
@@ -1728,6 +2160,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)}
</Button>
</div>
{detectionNotice && (
<p className={`text-xs text-center ${detectionNotice.found ? "text-emerald-400" : "text-muted-foreground"}`}>
{detectionNotice.text}
</p>
)}
</CardContent>
</Card>
)}
@@ -1738,19 +2175,15 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
// Version tracking is on when installed_via is set. Without a
// method the app is register-only — no cards, no warnings.
const tracking = !!app.installed_via
const hasUpdate = tracking && st?.update_available === true
const upToDate = tracking && st?.update_available === false && !!st?.installed_version
return (
<Card key={app.id} className="border border-border bg-card/50">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
{app.logo_url && (
<img
<ThemeAwareLogo
src={app.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
/>
)}
<div className="min-w-0 flex-1">
@@ -1801,30 +2234,17 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)}
</div>
{/* Version panels — always same-line (grid-cols-2), on
every viewport when ANY upstream source is configured
(github repo, http_json endpoint, or docker_hub
image). The signal that an update exists is the
LATEST UPSTREAM number turning purple + a small
ArrowUpCircle next to it, matching the project-wide
update button convention. Absence of purple = up to
date; no colored banners, no green/orange noise.
Labels stay muted so numbers keep visual priority.
When no upstream is configured, the LATEST panel is
omitted entirely and INSTALLED takes the full width. */}
{(() => {
const hasUpstream = !!(app.repo || app.upstream_type)
const hasUpdate = st?.update_available === true
if (!tracking || !(st?.installed_version || hasUpstream)) return null
return (
<div className={"mb-3 grid gap-3 " + (hasUpstream ? "grid-cols-2" : "grid-cols-1")}>
<div className={"mb-3 grid gap-3 " + (st?.installed_version && hasUpstream ? "grid-cols-2" : "grid-cols-1")}>
{st?.installed_version && (
<div className="p-3 rounded-md bg-muted/40">
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.installedStatus")}</div>
<div className="text-lg font-semibold font-mono text-foreground flex items-center gap-2">
<div className="text-lg font-semibold font-mono text-foreground">
{st.installed_version}
{upToDate && (
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
)}
</div>
</div>
)}
@@ -1867,11 +2287,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
return (
<div key={p.port} className="flex items-start gap-3 min-w-0">
{p.logo_url && (
<img
<ThemeAwareLogo
src={p.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
/>
)}
<div className="min-w-0 flex flex-col">
@@ -1896,8 +2314,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
{/* Footer with per-card actions — only rendered in the
global edit mode (toggled from the "Edit" button next
to Add another application). View mode keeps cards
chrome-free; Check is still reachable via the
hover-reveal icon on the LATEST panel. Buttons match
chrome-free; Check remains available in edit mode.
Buttons match
the Settings-page section style (h-8, outline,
small icon + label) for visual consistency across
the app. */}
@@ -1984,7 +2402,20 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
detections, so the user gets one-click Restore before hand-
typing a custom app. */}
{apps.length > 0 && (
<div className="flex justify-end items-center gap-2">
<div className="flex flex-wrap justify-end items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={searchInstalledApplications}
disabled={searchingApplications || editMode}
>
{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")}
</Button>
<Button
variant="outline"
size="sm"
@@ -2019,6 +2450,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div>
)}
{apps.length > 0 && detectionNotice && (
<p className={`text-xs text-right ${detectionNotice.found ? "text-emerald-400" : "text-muted-foreground"}`}>
{detectionNotice.text}
</p>
)}
{error && (
<div className="text-xs text-red-400 flex items-start gap-1.5">
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
File diff suppressed because it is too large Load Diff
+39 -8
View File
@@ -1,5 +1,5 @@
// Shared cross-component cache for the LXC App-tab payload
// (registered sidecar + auto-detected suggestions). Used by both
// (registered sidecar + cached detection suggestions). Used by both
// virtual-machines.tsx (which prefetches on modal open and on hover)
// and lxc-app-panel.tsx (which reads the cache first and only fetches
// if empty). The in-flight promise map dedups concurrent requests: if
@@ -16,14 +16,37 @@ export type LxcAppsBundle = {
const dataCache = new Map<number, LxcAppsBundle>()
const inFlight = new Map<number, Promise<LxcAppsBundle | null>>()
const cacheRevision = new Map<number, number>()
export function getLxcAppsCached(vmid: number): LxcAppsBundle | undefined {
return dataCache.get(vmid)
}
// Write-through after a successful App/Updates mutation. The API returns the
// complete sidecar, so evicting this entry would throw away newer data and
// make the App tab flash "Loading applications..." on its next mount. Keep
// the already-fetched suggestions unless the caller explicitly replaces them.
export function setLxcAppsCached(
vmid: number,
sidecar: any,
suggestions?: any | null,
): LxcAppsBundle {
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
const current = dataCache.get(vmid)
const bundle: LxcAppsBundle = {
sidecar,
suggestions: suggestions === undefined
? (current?.suggestions ?? null)
: suggestions,
}
dataCache.set(vmid, bundle)
return bundle
}
export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
const existing = inFlight.get(vmid)
if (existing) return existing
const startedRevision = cacheRevision.get(vmid) || 0
const p = Promise.all([
fetchApi(`/api/vms/${vmid}/apps`).catch(() => null) as Promise<any>,
fetchApi(`/api/vms/${vmid}/apps/suggestions`).catch(() => null) as Promise<any>,
@@ -31,6 +54,11 @@ export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
.then(([sc, sug]) => {
if (!sc) return null
const bundle: LxcAppsBundle = { sidecar: sc, suggestions: sug }
// A successful write may have completed while these GETs were in
// flight. Never let that older response overwrite the mutation result.
if ((cacheRevision.get(vmid) || 0) !== startedRevision) {
return dataCache.get(vmid) ?? null
}
dataCache.set(vmid, bundle)
return bundle
})
@@ -42,17 +70,20 @@ export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
}
export function invalidateLxcApps(vmid: number): void {
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
dataCache.delete(vmid)
}
// Seed the cache with a sidecar payload from the bulk modal-cache
// endpoint. Only the sidecar side is populated — suggestions still
// resolve lazily when the App panel actually mounts (the bulk
// endpoint intentionally excludes them since most guests don't
// need the auto-detected chips and the payload would balloon).
export function seedLxcAppsCache(vmid: number, sidecar: any): void {
// Seed the cache from the bulk modal-cache endpoint. Both registered
// apps and startup detection suggestions are already in memory, so
// opening the App tab never starts a discovery scan.
export function seedLxcAppsCache(
vmid: number,
sidecar: any,
suggestions?: any | null,
): void {
if (!sidecar) return
const existing = dataCache.get(vmid)
if (existing) return // per-panel fetch already ran, don't overwrite
dataCache.set(vmid, { sidecar, suggestions: null })
dataCache.set(vmid, { sidecar, suggestions: suggestions ?? null })
}
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "Betriebssystem + Anwendung",
"lastRun": "Letzte Ausführung: {date}",
"runSuccess": "✓ Erfolg",
"runPartial": "teilweise abgeschlossen",
"runFailed": "✗ fehlgeschlagen",
"runDeferred": "zur Sicherheit zurückgestellt",
"runSkipped": "nichts ausstehend",
"releaseDelaySummary": "{days} Tag(e) Freigabesperre",
"releaseDelayLabel": "Nach neuer App-Version warten",
"releaseDelayNone": "Keine Wartezeit",
"releaseDelayDays": "{days} Tag(e) warten",
"releaseDelayHelp": "Gilt nur für ausgewählte Apps mit Versionsüberwachung. Deren ausstehende Versionen müssen dieses Alter erreicht haben; Apps ohne Versionsüberwachung führen ihren Updater bei jedem Zeitplan aus.",
"notScheduled": "Nicht geplant",
"frequency": "Frequenz",
"cronExpression": "Cron-Ausdruck",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Nur Betriebssystempakete",
"targetOptionApp": "Nur Bewerbung",
"targetOptionBoth": "Betriebssystem + Anwendung",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Docker-Container · {container}",
"dockerEngineTarget": "Docker Engine",
"noTargets": "Keine Ziele ausgewählt",
"selectAtLeastOne": "Wählen Sie mindestens ein Aktualisierungsziel aus.",
"deleteButton": "Zeitplan löschen",
"deleteConfirm": "Geplante Updates für diesen Container entfernen? Die Standardeinstellungen (Sicherung + Neustart) werden beibehalten."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "Betriebssystem + {appName}-Updates",
"osPlusApps": "Betriebssystem- und Apps-Updates",
"noUpdateMethodTitle": "Keine Aktualisierungsmethode verfügbar",
"noUpdateMethodBody": "Keine Aktualisierungsmethode verfügbar. Aktualisieren Sie über die App selbst oder legen Sie einen benutzerdefinierten Befehl auf der Registerkarte „App“ fest.",
"noUpdateMethodBody": "Für diese Anwendung wurde keine Aktualisierungsmethode erkannt. Fügen Sie einen benutzerdefinierten Aktualisierungsbefehl hinzu.",
"ociImmutableTitle": "OCI-Image-Container",
"ociImmutableBody": "Betriebssystempakete werden zum Zeitpunkt der Image-Erstellung integriert und können nicht direkt aktualisiert werden. Wenden Sie Updates manuell an oder installieren Sie sie mit einem neueren Image neu.",
"hideNoticeButton": "Diesen Hinweis für diese App ausblenden",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Anwendung",
"installedLabel": "installiert",
"upToDateAtLabel": "Aktuell unter",
"versionLabel": "Version {version}",
"versionTrackingPending": "Versionsverfolgung ausstehend – bei der nächsten geplanten Prüfung werden die installierte und die verfügbare Version angezeigt.",
"customCommandTitle": "Benutzerdefinierter Update-Befehl",
"customCommandBody": "Führen Sie einen benutzerdefinierten Shell-Befehl aus, um diese App im Container zu aktualisieren.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Diesen Hinweis für {appName} ausblenden",
"detectedByPrefix": "Erkannt von",
"editApp": "Bearbeiten",
"configureUpdater": "Konfigurieren",
"alsoDetectedTitle": "Außerdem in diesem Container entdeckt:",
"detectedInline": "· erkannt von {method}",
"customCommandLabel": "Benutzerdefinierter Update-Befehl",
@@ -1245,9 +1260,9 @@
"removeButton": "Entfernen",
"cancelButton": "Stornieren",
"saveButton": "Speichern",
"editCommandButton": "Befehl bearbeiten",
"wireUpCommandButton": "Fügen Sie einen benutzerdefinierten Aktualisierungsbefehl hinzu",
"versionTrackingPendingShort": "Versionsverfolgung steht aus.",
"versionTrackingNotConfigured": "Die Versionsüberwachung ist nicht konfiguriert.",
"managedByOsPackages": "Diese Anwendung wird über die Aktion für Betriebssystempakete aktualisiert.",
"noMethodBody": "Keine Aktualisierungsmethode verfügbar. Aktualisieren Sie über die App selbst oder verknüpfen Sie einen benutzerdefinierten Befehl, den ProxMenux im Container ausführt.",
"noMethodHideBody": "Für diese App ist keine Update-Methode registriert. Legen Sie eine auf der Registerkarte „App“ fest oder schließen Sie diesen Hinweis.",
"hideForApp": "Hinweis für {appName} ausblenden",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Noch keine Update-Informationen – prüfen Sie unter Sicherheit → Secure Gateway.",
"ociTitle": "OCI-Image-Container",
"ociBody": "Dieser Container wurde aus einem OCI-Image (Docker) erstellt. Die Updateverwaltung für OCI-Container wird mit der kommenden OCI-Installationsfunktion geliefert – Updates erstellen den Container anhand eines neueren Image-Tags neu, anstatt Pakete darin zu patchen.",
"helperNotUpdateable": "Die Community-Scripts-Registrierung markiert diese App als nicht aktualisierbar.",
"helperDetectedTitle": "Es wurde ein Hilfsskript-Updater erkannt",
"helperDetectedBody": "Die manuelle Anwendung ist am sichersten.",
"dockerImagesTitle": "Docker-Images",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
"dockerImagesReadOnly": "Image-Tags werden anhand des Registry-Digests verglichen. Compose-Dienste können aus ihrem deklarierten Projekt geladen und neu erstellt werden.",
"dockerEngineManagedByOs": "Docker Engine {version} wird über Pakete verwaltet und durch die obige Betriebssystem-Paketaktion aktualisiert.",
"dockerEngineSubheading": "Engine",
"dockerEngineDetected": "Docker Engine {version} erkannt",
"dockerEngineUpdateHelp": "Aktualisiert nur die installierten Docker-Pakete und erforderlichen Abhängigkeiten. Andere Betriebssystempakete und Container bleiben unverändert.",
"updateDockerEngineOnly": "Docker Engine aktualisieren",
"adguardWebUpdateOnly": "Diese AdGuard-Home-Installation wird über die eigene Weboberfläche aktualisiert.",
"openAdguard": "AdGuard Home öffnen",
"updateDockerImage": "Image aktualisieren",
"recreateStandaloneContainer": "{container} aktualisieren",
"standaloneRecreatePending": "Eigenständige Container benötigen eine geschützte Neuerstellung: {names}",
"noDockerImages": "Docker ist installiert, aber es wurden keine getaggten Images gefunden.",
"dockerInventoryStarting": "Warten, bis Docker vollständig gestartet ist…",
"dockerInventoryUnavailable": "Das Docker-Inventar konnte nicht gelesen werden. Prüfen Sie es erneut, sobald Docker verfügbar ist.",
"usedByContainers": "Verwendet von: {names}",
"imageUpdateAvailable": "Neues Image verfügbar",
"imageUpToDate": "Aktuell",
"imageInstalledTag": "installierter Tag",
"imageDigestUnknown": "Digest nicht verfügbar",
"dockerPendingSummary": "{count} Docker-Image-Update(s) erkannt. Aktualisiere sie mit dem zugehörigen Docker- oder Compose-Workflow.",
"postApplyChecking": "Aktualisierungsergebnis wird überprüft…",
"postApplyAllOk": "{count} Paket(e) erfolgreich angewendet – nichts ausstehend.",
"postApplyNothingPending": "Nichts ausstehend – alles ist auf dem neuesten Stand.",
"postApplyPartial": "{pending} Paket(e) stehen nach der Ausführung noch aus.",
"postApplyPartialSubline": "{applied} angewendet.Einige Aktualisierungen wurden nicht abgeschlossen – sehen Sie sich die Terminalausgabe oben an."
},
"bulkUpdate": {
"title": "Sammelaktualisierung",
"description": "Konfiguriert eine Aktion für das Betriebssystem und die ausgewählten Aktualisierungsmethoden dieses Containers.",
"loading": "Konfiguration wird geladen…",
"notConfigured": "Nicht konfiguriert.",
"configure": "Konfigurieren",
"edit": "Bearbeiten",
"apply": "Aktualisierungen anwenden",
"osTarget": "Betriebssystem",
"osRequired": "Für eine Sammelaktualisierung erforderlich.",
"includesDependencies": "Enthält auch deklarierte Abhängigkeiten: {names}",
"noMethod": "Keine ausführbare Aktualisierungsmethode verfügbar.",
"staleTarget": "Diese Auswahl ist nicht mehr verfügbar. Entfernen Sie sie vor dem Speichern.",
"dockerInventoryPending": "Warten auf das Docker-Image-Inventar…",
"missingDockerTarget": "Docker-Image nicht verfügbar",
"selectAtLeastOne": "Wählen Sie zusätzlich zum Betriebssystem mindestens ein Aktualisierungsziel.",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Konfiguration entfernen",
"deleteConfirm": "Diese Sammelaktualisierungskonfiguration entfernen?",
"saveFailed": "Die Sammelaktualisierungskonfiguration konnte nicht gespeichert werden.",
"deleteFailed": "Die Sammelaktualisierungskonfiguration konnte nicht entfernt werden.",
"planFailed": "Die Sammelaktualisierung konnte nicht vorbereitet werden. Prüfen Sie die nicht verfügbaren Methoden."
},
"appEditor": {
"closePanel": "Panel schließen",
"cancelButton": "Stornieren",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Derzeit ausgeblendet – wird wieder in der Erkennungsliste angezeigt",
"registerDifferent": "Registrieren Sie eine andere App",
"detectedInContainer": "Auf diesem Container erkannt",
"versionDetected": "Version {version} in diesem Container erkannt",
"legacyDetectorUsed": "Mit einem alten Kompatibilitäts-Fallback erkannt",
"dockerImagesTitle": "Docker-Image-Updates",
"dockerImagesHelp": "Prüft jedes lokale Image getrennt von der Docker-Engine anhand unveränderlicher Registry-Digests.",
"refreshDockerImages": "Jetzt prüfen",
"noDockerImages": "Keine getaggten Docker-Images gefunden",
"usedByContainers": "Verwendet von: {names}",
"imageUpdateAvailable": "Neues Image verfügbar",
"imageUpToDate": "Aktuell",
"imageDigestUnknown": "Digest nicht verfügbar",
"dockerReadOnlyNote": "Nur-Lese-Prüfung: Es wird kein Image geladen und kein Container neu gestartet.",
"installedManaged": "Installiert und verwaltet von ProxMenux",
"nameLabel": "Name",
"installedViaLabel": "Installiert über",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "z. B. data.version oder releases[0].tag_name",
"dockerImageLabel": "Docker Hub-Bild",
"dockerImagePlaceholder": "z. B. linuxserver/plex oder nginx",
"dockerVersionedTagsHelp": "Nur für versionierte Tags. Für latest, stable oder lts die Docker-Image-Updates mit Digest-Vergleich verwenden.",
"dockerPresetSemver": "Nur SemVer",
"dockerPresetSemverSuffix": "SemVer + Suffix",
"dockerTagFilterHelp": "Filtert echte Tags, bevor ProxMenux die höchste Version auswählt.",
"dockerTagPreviewLabel": "Live-Tag-Vorschau",
"dockerTagPreviewCount": "{matched} von {scanned} passend",
"dockerTagPreviewLoading": "Docker Hub wird geprüft…",
"dockerTagPreviewFailed": "Docker-Hub-Tags konnten nicht angezeigt werden",
"dockerTagPreviewEmpty": "Keine echten Tags entsprechen diesem Filter.",
"dockerMovingTag": "beweglicher Tag",
"dockerMovingTagHelp": "Bewegliche Tags enthalten keine Version. Stattdessen über den Digest bei Docker-Image-Updates verfolgen.",
"tagRegexLabel": "Tag-Regex (mit Capture-Gruppe)",
"tagRegexPlaceholder": "z.B. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registrieren Sie eine benutzerdefinierte App",
"noAppsTitle": "Keine Bewerbungen registriert",
"noAppsBody": "Registrieren Sie die Anwendungen in diesem Container, um Weblinks und optional die Verfolgung verfügbarer Versionen mit Benachrichtigungen zu neuen Releases zu erhalten.",
"registerApplication": "Bewerbung registrieren",
"registerApplication": "Anwendung registrieren",
"searchApplications": "Apps suchen",
"searchingApplications": "Apps werden gesucht…",
"noApplicationsDetected": "Keine Apps erkannt.",
"noNewApplicationsDetected": "Keine neuen Apps erkannt.",
"oneNewApplicationDetected": "Eine neue App wurde erkannt.",
"newApplicationsDetected": "{count} neue Apps wurden erkannt.",
"detectionFailed": "App-Suche fehlgeschlagen",
"hiddenSuffix": "{count} ausgeblendet",
"confirmHide": "Die Erkennung „{name}“ dauerhaft in diesem Container ausblenden?\n\nUm es später wiederherzustellen, klicken Sie auf „Andere App registrieren“ – die ausgeblendete Liste wird dort mit einer Wiederherstellungsoption angezeigt.",
"saveFailed": "Speichern fehlgeschlagen",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana braucht",
"loadFailed": "Die App-Konfiguration konnte nicht geladen werden",
"checkFailed": "Prüfung fehlgeschlagen",
"testDetectorButton": "Detektor testen",
"testingDetectorButton": "Detektor wird getestet…",
"detectorTestTitle": "Ergebnis des Detektortests",
"detectorTestNotSaved": "Nur Entwurf · nicht gespeichert",
"detectorTestNoVersion": "Keine Version erkannt",
"detectorTestNoUpstream": "Keine Upstream-Quelle konfiguriert",
"detectorTestFailed": "Detektortest fehlgeschlagen",
"deleteFailed": "Das Löschen ist fehlgeschlagen",
"dismissFailed": "Die Erkennung konnte nicht verworfen werden",
"restoreFailed": "Die Erkennung konnte nicht wiederhergestellt werden",
@@ -1399,6 +1494,10 @@
"webLinks": "Weblinks",
"addPort": "Port hinzufügen",
"detectedPorts": "Im Container erkannte Ports – klicken Sie zum Hinzufügen:",
"dockerPublishedServicesTitle": "Docker-Dienste mit veröffentlichten Ports",
"dockerPublishedServicesHelp": "Wählen Sie nur Ports aus, die eine Weboberfläche bereitstellen. ProxMenux registriert diese Container nicht als eigenständige LXC-Anwendungen.",
"dockerPublishedAdd": "Link hinzufügen",
"dockerPublishedPort": "Container-Port {containerPort} → {hostPort} im LXC",
"noWebPorts": "Keine Web-Ports lauschen. Verwenden Sie Port hinzufügen, wenn Sie dennoch manuell einen Link erstellen möchten.",
"trackUpstream": "Verfügbare Version verfolgen (optional)",
"trackOff": "Aus – nur Link",
@@ -1416,7 +1515,7 @@
"checkButton": "Überprüfen",
"editFieldsButton": "Felder bearbeiten",
"alsoDetectedContainer": "Auch auf diesem Container erkannt",
"addAnotherApplication": "Fügen Sie eine weitere Anwendung hinzu",
"addAnotherApplication": "Weitere Anwendung registrieren",
"doneButton": "Erledigt",
"editButton": "Bearbeiten",
"upstreamErrorTimeout": "Netzwerk-Timeout beim Kontaktieren des Upstreams",
+106 -7
View File
@@ -1162,7 +1162,15 @@
"targetBoth": "OS + application",
"lastRun": "Last run: {date}",
"runSuccess": "✓ success",
"runPartial": "completed partially",
"runFailed": "✗ failed",
"runDeferred": "held for safety",
"runSkipped": "nothing pending",
"releaseDelaySummary": "{days}-day release hold",
"releaseDelayLabel": "Wait after a new app release",
"releaseDelayNone": "No waiting period",
"releaseDelayDays": "Wait {days} day(s)",
"releaseDelayHelp": "Applies only to selected apps with version tracking. Their pending releases must be this old; apps without version tracking still run their updater on every scheduled occurrence.",
"notScheduled": "Not scheduled",
"frequency": "Frequency",
"cronExpression": "Cron expression",
@@ -1171,6 +1179,11 @@
"targetOptionOs": "OS packages only",
"targetOptionApp": "Application only",
"targetOptionBoth": "OS + application",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Docker container · {container}",
"dockerEngineTarget": "Docker Engine",
"noTargets": "No targets selected",
"selectAtLeastOne": "Select at least one update target.",
"deleteButton": "Delete schedule",
"deleteConfirm": "Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept."
},
@@ -1216,7 +1229,7 @@
"osPlusApp": "OS + {appName} updates",
"osPlusApps": "OS + Apps updates",
"noUpdateMethodTitle": "No update method available",
"noUpdateMethodBody": "No update method available. Update from the app itself, or set a custom command in the App tab.",
"noUpdateMethodBody": "No update method has been identified for this application. Add a custom update command.",
"ociImmutableTitle": "OCI image container",
"ociImmutableBody": "OS packages are baked in at image build time and cannot be updated in place. Apply updates manually or reinstall with a newer image.",
"hideNoticeButton": "Hide this notice for this app",
@@ -1229,6 +1242,7 @@
"applicationDefaultName": "Application",
"installedLabel": "installed",
"upToDateAtLabel": "Up to date at",
"versionLabel": "version {version}",
"versionTrackingPending": "Version tracking pending — the next scheduled check will populate installed and upstream numbers.",
"customCommandTitle": "Custom update command",
"customCommandBody": "Run a user-defined shell command to update this app inside the container.",
@@ -1237,6 +1251,7 @@
"hideNoticeAction": "Hide this notice for {appName}",
"detectedByPrefix": "Detected by",
"editApp": "Edit",
"configureUpdater": "Configure",
"alsoDetectedTitle": "Also detected in this container:",
"detectedInline": "· detected by {method}",
"customCommandLabel": "Custom update command",
@@ -1244,9 +1259,9 @@
"removeButton": "Remove",
"cancelButton": "Cancel",
"saveButton": "Save",
"editCommandButton": "Edit command",
"wireUpCommandButton": "Add custom update command",
"versionTrackingPendingShort": "Version tracking pending.",
"versionTrackingNotConfigured": "Version tracking is not configured.",
"managedByOsPackages": "This application is updated by the OS packages action.",
"noMethodBody": "No update method available. Update from the app itself, or wire up a custom command that ProxMenux will run inside the container.",
"noMethodHideBody": "This app has no update method registered. Set one from the App tab or dismiss this notice.",
"hideForApp": "Hide notice for {appName}",
@@ -1265,15 +1280,59 @@
"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.",
"helperNotUpdateable": "The community-scripts registry marks this app as not updateable.",
"helperDetectedTitle": "Detected a helper-scripts updater",
"helperDetectedBody": "Applying manually is safest.",
"dockerImagesTitle": "Docker images",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
"dockerImagesReadOnly": "Image tags are compared by registry digest. Compose services can be pulled and recreated from their declared project.",
"dockerEngineManagedByOs": "Docker Engine {version} is package-managed and is updated by the OS packages action above.",
"dockerEngineSubheading": "Engine",
"dockerEngineDetected": "Docker Engine {version} detected",
"dockerEngineUpdateHelp": "Updates only the installed Docker packages and required dependencies. It does not upgrade other OS packages or recreate containers.",
"updateDockerEngineOnly": "Update Docker Engine",
"adguardWebUpdateOnly": "This AdGuard Home installation is updated from its own web interface.",
"openAdguard": "Open AdGuard Home",
"updateDockerImage": "Update image",
"recreateStandaloneContainer": "Update {container}",
"standaloneRecreatePending": "Standalone container(s) require protected recreation: {names}",
"noDockerImages": "Docker is installed, but no tagged images were found.",
"dockerInventoryStarting": "Waiting for Docker to finish starting…",
"dockerInventoryUnavailable": "The Docker inventory could not be read. Check again when Docker is available.",
"usedByContainers": "Used by: {names}",
"imageUpdateAvailable": "New image available",
"imageUpToDate": "Up to date",
"imageInstalledTag": "installed tag",
"imageDigestUnknown": "Digest unavailable",
"dockerPendingSummary": "{count} Docker image update(s) detected. Update with the owning Docker or Compose workflow.",
"postApplyChecking": "Verifying update result…",
"postApplyAllOk": "{count} package(s) applied successfully — nothing pending.",
"postApplyNothingPending": "Nothing pending — everything is up to date.",
"postApplyPartial": "{pending} package(s) still pending after the run.",
"postApplyPartialSubline": "{applied} applied. Some updates did not complete — review the terminal output above."
},
"bulkUpdate": {
"title": "Bulk update",
"description": "Configure one action to update the OS and the methods you select in this container.",
"loading": "Loading configuration…",
"notConfigured": "Not configured.",
"configure": "Configure",
"edit": "Edit",
"apply": "Apply updates",
"osTarget": "OS",
"osRequired": "Required for a bulk update.",
"includesDependencies": "Also includes declared dependencies: {names}",
"noMethod": "No executable update method is available.",
"staleTarget": "This selection is no longer available. Remove it before saving.",
"dockerInventoryPending": "Waiting for the Docker image inventory…",
"missingDockerTarget": "Docker image unavailable",
"selectAtLeastOne": "Select at least one update target in addition to the OS.",
"cancel": "Cancel",
"save": "Save",
"delete": "Remove configuration",
"deleteConfirm": "Remove this bulk update configuration?",
"saveFailed": "Could not save the bulk update configuration.",
"deleteFailed": "Could not remove the bulk update configuration.",
"planFailed": "Could not prepare the bulk update. Edit the configuration and review unavailable methods."
},
"appEditor": {
"closePanel": "Close panel",
"cancelButton": "Cancel",
@@ -1283,6 +1342,17 @@
"hiddenBadge": "Currently hidden — will re-appear in the detection list",
"registerDifferent": "Register a different app",
"detectedInContainer": "Detected on this container",
"versionDetected": "Version {version} detected in this container",
"legacyDetectorUsed": "Detected with a legacy compatibility fallback",
"dockerImagesTitle": "Docker image updates",
"dockerImagesHelp": "Tracks each local image separately from the Docker engine by comparing immutable registry digests.",
"refreshDockerImages": "Check now",
"noDockerImages": "No tagged Docker images found",
"usedByContainers": "Used by: {names}",
"imageUpdateAvailable": "New image available",
"imageUpToDate": "Up to date",
"imageDigestUnknown": "Digest unavailable",
"dockerReadOnlyNote": "Read-only check: no image is pulled and no container is restarted.",
"installedManaged": "Installed and managed by ProxMenux",
"nameLabel": "Name",
"installedViaLabel": "Installed via",
@@ -1339,6 +1409,17 @@
"jsonPathPlaceholder": "e.g., data.version or releases[0].tag_name",
"dockerImageLabel": "Docker Hub image",
"dockerImagePlaceholder": "e.g., linuxserver/plex or nginx",
"dockerVersionedTagsHelp": "Use this only for versioned tags. For latest, stable, or lts, use Docker image updates, which compares digests.",
"dockerPresetSemver": "SemVer only",
"dockerPresetSemverSuffix": "SemVer + suffix",
"dockerTagFilterHelp": "Filters real tags before ProxMenux selects the highest version.",
"dockerTagPreviewLabel": "Live tag preview",
"dockerTagPreviewCount": "{matched} matched of {scanned}",
"dockerTagPreviewLoading": "Checking Docker Hub…",
"dockerTagPreviewFailed": "Could not preview Docker Hub tags",
"dockerTagPreviewEmpty": "No real tags match this filter.",
"dockerMovingTag": "moving tag",
"dockerMovingTagHelp": "Moving tags do not contain a version. Track them in Docker image updates by digest instead.",
"tagRegexLabel": "Tag regex (with capture group)",
"tagRegexPlaceholder": "e.g., v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1375,6 +1456,13 @@
"noAppsTitle": "No applications registered",
"noAppsBody": "Register each application running in this container. Get clickable web links, and — optionally — upstream version tracking + notifications for new releases.",
"registerApplication": "Register application",
"searchApplications": "Find applications",
"searchingApplications": "Searching applications…",
"noApplicationsDetected": "No applications were detected.",
"noNewApplicationsDetected": "No new applications were detected.",
"oneNewApplicationDetected": "One new application was detected.",
"newApplicationsDetected": "{count} new applications were detected.",
"detectionFailed": "Application detection failed",
"hiddenSuffix": "{count} hidden",
"confirmHide": "Hide the \"{name}\" detection permanently on this container?\n\nTo bring it back later, click \"Register a different app\" — the hidden list appears there with a Restore option.",
"saveFailed": "Save failed",
@@ -1385,6 +1473,13 @@
"binaryArgsHintGrafana": "Grafana needs",
"loadFailed": "Could not load app configuration",
"checkFailed": "Check failed",
"testDetectorButton": "Test detector",
"testingDetectorButton": "Testing detector…",
"detectorTestTitle": "Detector test result",
"detectorTestNotSaved": "Draft only · not saved",
"detectorTestNoVersion": "No version was detected",
"detectorTestNoUpstream": "No upstream source configured",
"detectorTestFailed": "Detector test failed",
"deleteFailed": "Delete failed",
"dismissFailed": "Could not dismiss detection",
"restoreFailed": "Could not restore detection",
@@ -1401,6 +1496,10 @@
"webLinks": "Web links",
"addPort": "Add port",
"detectedPorts": "Ports detected in the container — click to add:",
"dockerPublishedServicesTitle": "Docker services with published ports",
"dockerPublishedServicesHelp": "Choose only ports that provide a web interface. ProxMenux does not register these containers as independent LXC applications.",
"dockerPublishedAdd": "Add link",
"dockerPublishedPort": "container port {containerPort} → {hostPort} on the LXC",
"noWebPorts": "No web ports listening. Use Add port if you still want to create a link manually.",
"trackUpstream": "Track upstream version (optional)",
"trackOff": "Off — link only",
@@ -1418,7 +1517,7 @@
"checkButton": "Check",
"editFieldsButton": "Edit fields",
"alsoDetectedContainer": "Also detected on this container",
"addAnotherApplication": "Add another application",
"addAnotherApplication": "Register another application",
"doneButton": "Done",
"editButton": "Edit",
"notificationsEnabled": "Upstream update notifications ON — click to mute",
+108 -9
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "SO + aplicación",
"lastRun": "Última ejecución: {date}",
"runSuccess": "✓ éxito",
"runPartial": "completada parcialmente",
"runFailed": "✗ falló",
"runDeferred": "retenida por seguridad",
"runSkipped": "nada pendiente",
"releaseDelaySummary": "espera de {days} día(s)",
"releaseDelayLabel": "Esperar tras una nueva versión de app",
"releaseDelayNone": "Sin periodo de espera",
"releaseDelayDays": "Esperar {days} día(s)",
"releaseDelayHelp": "Solo se aplica a las apps seleccionadas con seguimiento de versión. Sus versiones pendientes deben tener esta antigüedad; las apps sin seguimiento ejecutan su actualizador en cada programación.",
"notScheduled": "No programado",
"frequency": "Frecuencia",
"cronExpression": "expresión cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Solo paquetes de sistema operativo",
"targetOptionApp": "Sólo aplicación",
"targetOptionBoth": "SO + aplicación",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Contenedor Docker · {container}",
"dockerEngineTarget": "Motor de Docker",
"noTargets": "Ningún objetivo seleccionado",
"selectAtLeastOne": "Seleccione al menos un objetivo de actualización.",
"deleteButton": "Eliminar horario",
"deleteConfirm": "¿Eliminar las actualizaciones programadas para este contenedor? Se mantienen los valores predeterminados de aplicación (copia de seguridad + reinicio)."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "SO + actualizaciones {appName}",
"osPlusApps": "Actualizaciones de SO y aplicaciones",
"noUpdateMethodTitle": "No hay ningún método de actualización disponible",
"noUpdateMethodBody": "No hay ningún método de actualización disponible. Actualice desde la propia aplicación o configure un comando personalizado en la pestaña Aplicación.",
"noUpdateMethodBody": "No se ha identificado un método de actualización para esta aplicación. Añada un comando de actualización personalizado.",
"ociImmutableTitle": "Contenedor de imágenes OCI",
"ociImmutableBody": "Los paquetes del sistema operativo se integran en el momento de crear la imagen y no se pueden actualizar en el lugar. Aplique las actualizaciones manualmente o reinstálelas con una imagen más nueva.",
"hideNoticeButton": "Ocultar este aviso para esta aplicación",
@@ -1229,7 +1242,8 @@
"securityUpdatesLabel": "actualizaciones de seguridad",
"applicationDefaultName": "Solicitud",
"installedLabel": "instalado",
"upToDateAtLabel": "Al día en",
"upToDateAtLabel": "Actualizada en la versión",
"versionLabel": "versión {version}",
"versionTrackingPending": "Seguimiento de versiones pendiente: la próxima comprobación programada mostrará las versiones instalada y disponible.",
"customCommandTitle": "Comando de actualización personalizado",
"customCommandBody": "Ejecute un comando de shell definido por el usuario para actualizar esta aplicación dentro del contenedor.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Ocultar este aviso para {appName}",
"detectedByPrefix": "Detectado por",
"editApp": "Editar",
"configureUpdater": "Configurar",
"alsoDetectedTitle": "También detectado en este contenedor:",
"detectedInline": "· detectado por {method}",
"customCommandLabel": "Comando de actualización personalizado",
@@ -1245,9 +1260,9 @@
"removeButton": "Eliminar",
"cancelButton": "Cancelar",
"saveButton": "Guardar",
"editCommandButton": "Editar comando",
"wireUpCommandButton": "Agregar comando de actualización personalizado",
"versionTrackingPendingShort": "Seguimiento de versión pendiente.",
"versionTrackingNotConfigured": "El seguimiento de versión no está configurado.",
"managedByOsPackages": "Esta aplicación se actualiza mediante la acción de paquetes del SO.",
"noMethodBody": "No hay ningún método de actualización disponible. Actualice desde la propia aplicación o conecte un comando personalizado que ProxMenux ejecutará dentro del contenedor.",
"noMethodHideBody": "Esta aplicación no tiene ningún método de actualización registrado. Configure uno desde la pestaña Aplicación o ignore este aviso.",
"hideForApp": "Ocultar aviso para {appName}",
@@ -1266,15 +1281,59 @@
"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.",
"helperNotUpdateable": "El registro de scripts comunitarios marca esta aplicación como no actualizable.",
"helperDetectedTitle": "Detectado un actualizador de scripts auxiliares",
"helperDetectedBody": "La aplicación manual es la más segura.",
"dockerImagesTitle": "Imágenes Docker",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Imágenes",
"dockerImagesReadOnly": "Las etiquetas se comparan mediante el digest del registro. Los servicios Compose pueden descargar la imagen y recrearse desde su proyecto declarado.",
"dockerEngineManagedByOs": "Docker Engine {version} está gestionado por paquetes y se actualiza mediante la acción de paquetes del SO anterior.",
"dockerEngineSubheading": "Motor",
"dockerEngineDetected": "Docker Engine {version} detectado",
"dockerEngineUpdateHelp": "Actualiza únicamente los paquetes Docker instalados y sus dependencias necesarias. No actualiza otros paquetes del SO ni recrea contenedores.",
"updateDockerEngineOnly": "Actualizar Docker Engine",
"adguardWebUpdateOnly": "Esta instalación de AdGuard Home se actualiza desde su propia interfaz web.",
"openAdguard": "Abrir AdGuard Home",
"updateDockerImage": "Actualizar imagen",
"recreateStandaloneContainer": "Actualizar {container}",
"standaloneRecreatePending": "Estos contenedores independientes requieren una recreación protegida: {names}",
"noDockerImages": "Docker está instalado, pero no se encontraron imágenes con etiqueta.",
"dockerInventoryStarting": "Esperando a que Docker termine de arrancar…",
"dockerInventoryUnavailable": "No se pudo leer el inventario de Docker. Vuelva a comprobarlo cuando Docker esté disponible.",
"usedByContainers": "Usada por: {names}",
"imageUpdateAvailable": "Nueva imagen disponible",
"imageUpToDate": "Actualizada",
"imageInstalledTag": "etiqueta instalada",
"imageDigestUnknown": "Digest no disponible",
"dockerPendingSummary": "Se detectaron {count} actualización(es) de imágenes Docker. Actualízalas con su flujo de Docker o Compose.",
"postApplyChecking": "Comprobando resultado de la actualización…",
"postApplyAllOk": "{count} paquete(s) aplicados correctamente — nada pendiente.",
"postApplyNothingPending": "Nada pendiente — todo actualizado.",
"postApplyPartial": "{pending} paquete(s) siguen pendientes tras la ejecución.",
"postApplyPartialSubline": "{applied} aplicados. Algunas actualizaciones no finalizaron — revisa la salida del terminal."
},
"bulkUpdate": {
"title": "Actualización en bloque",
"description": "Configure una sola acción para actualizar el SO y los métodos que seleccione en este contenedor.",
"loading": "Cargando configuración…",
"notConfigured": "No configurada.",
"configure": "Configurar",
"edit": "Editar",
"apply": "Aplicar actualizaciones",
"osTarget": "SO",
"osRequired": "Obligatorio en una actualización en bloque.",
"includesDependencies": "Incluye también las dependencias declaradas: {names}",
"noMethod": "No hay ningún método de actualización ejecutable disponible.",
"staleTarget": "Esta selección ya no está disponible. Elimínela antes de guardar.",
"dockerInventoryPending": "Esperando al inventario de imágenes Docker…",
"missingDockerTarget": "Imagen Docker no disponible",
"selectAtLeastOne": "Seleccione al menos un método de actualización además del SO.",
"cancel": "Cancelar",
"save": "Guardar",
"delete": "Eliminar configuración",
"deleteConfirm": "¿Eliminar esta configuración de actualización en bloque?",
"saveFailed": "No se pudo guardar la configuración de actualización en bloque.",
"deleteFailed": "No se pudo eliminar la configuración de actualización en bloque.",
"planFailed": "No se pudo preparar la actualización en bloque. Edite la configuración y revise los métodos no disponibles."
},
"appEditor": {
"closePanel": "Cerrar panel",
"cancelButton": "Cancelar",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Actualmente oculto: volverá a aparecer en la lista de detección",
"registerDifferent": "Registrar una aplicación diferente",
"detectedInContainer": "Detectado en este contenedor",
"versionDetected": "Versión {version} detectada en este contenedor",
"legacyDetectorUsed": "Detectada mediante un fallback de compatibilidad antiguo",
"dockerImagesTitle": "Actualizaciones de imágenes Docker",
"dockerImagesHelp": "Comprueba cada imagen local por separado del motor Docker comparando digests inmutables del registro.",
"refreshDockerImages": "Comprobar ahora",
"noDockerImages": "No se encontraron imágenes Docker con etiqueta",
"usedByContainers": "Usada por: {names}",
"imageUpdateAvailable": "Nueva imagen disponible",
"imageUpToDate": "Actualizada",
"imageDigestUnknown": "Digest no disponible",
"dockerReadOnlyNote": "Comprobación de solo lectura: no descarga imágenes ni reinicia contenedores.",
"installedManaged": "Instalado y administrado por ProxMenux",
"nameLabel": "Nombre",
"installedViaLabel": "Instalado a través de",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "por ejemplo, data.version o lanzamientos[0].tag_name",
"dockerImageLabel": "Imagen de Docker Hub",
"dockerImagePlaceholder": "por ejemplo, linuxserver/plex o nginx",
"dockerVersionedTagsHelp": "Úsalo solo con etiquetas versionadas. Para latest, stable o lts, usa Actualizaciones de imágenes Docker, que compara digests.",
"dockerPresetSemver": "Solo SemVer",
"dockerPresetSemverSuffix": "SemVer + sufijo",
"dockerTagFilterHelp": "Filtra las etiquetas reales antes de que ProxMenux elija la versión más alta.",
"dockerTagPreviewLabel": "Vista previa de etiquetas reales",
"dockerTagPreviewCount": "{matched} coinciden de {scanned}",
"dockerTagPreviewLoading": "Consultando Docker Hub…",
"dockerTagPreviewFailed": "No se pudieron consultar las etiquetas de Docker Hub",
"dockerTagPreviewEmpty": "Ninguna etiqueta real coincide con este filtro.",
"dockerMovingTag": "etiqueta móvil",
"dockerMovingTagHelp": "Las etiquetas móviles no contienen una versión. Contrólalas por digest en Actualizaciones de imágenes Docker.",
"tagRegexLabel": "Etiquetar expresiones regulares (con grupo de captura)",
"tagRegexPlaceholder": "por ejemplo, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1375,7 +1456,14 @@
"registerCustom": "Registrar una aplicación personalizada",
"noAppsTitle": "No hay aplicaciones registradas",
"noAppsBody": "Registre las aplicaciones que se ejecutan en este contenedor. Obtendrá enlaces web y, opcionalmente, seguimiento de versiones disponibles y notificaciones de nuevos lanzamientos.",
"registerApplication": "Solicitud de registro",
"registerApplication": "Registrar aplicación",
"searchApplications": "Buscar aplicaciones",
"searchingApplications": "Buscando aplicaciones…",
"noApplicationsDetected": "No se han detectado aplicaciones.",
"noNewApplicationsDetected": "No se han detectado aplicaciones nuevas.",
"oneNewApplicationDetected": "Se ha detectado una aplicación nueva.",
"newApplicationsDetected": "Se han detectado {count} aplicaciones nuevas.",
"detectionFailed": "No se pudo completar la búsqueda de aplicaciones",
"hiddenSuffix": "{count} oculto",
"confirmHide": "¿Ocultar la detección \"{name}\" permanentemente en este contenedor?\n\nPara recuperarlo más tarde, haga clic en \"Registrar una aplicación diferente\"; la lista oculta aparece allí con una opción de Restaurar.",
"saveFailed": "Error al guardar",
@@ -1386,6 +1474,13 @@
"binaryArgsHintGrafana": "Grafana necesita",
"loadFailed": "No se pudo cargar la configuración de la aplicación",
"checkFailed": "Verificación fallida",
"testDetectorButton": "Probar detector",
"testingDetectorButton": "Probando detector…",
"detectorTestTitle": "Resultado de la prueba del detector",
"detectorTestNotSaved": "Solo borrador · sin guardar",
"detectorTestNoVersion": "No se detectó ninguna versión",
"detectorTestNoUpstream": "No hay una fuente upstream configurada",
"detectorTestFailed": "La prueba del detector ha fallado",
"deleteFailed": "Error al eliminar",
"dismissFailed": "No se pudo descartar la detección",
"restoreFailed": "No se pudo restaurar la detección",
@@ -1402,6 +1497,10 @@
"webLinks": "Enlaces web",
"addPort": "Agregar puerto",
"detectedPorts": "Puertos detectados en el contenedor: haga clic para agregar:",
"dockerPublishedServicesTitle": "Servicios Docker con puertos publicados",
"dockerPublishedServicesHelp": "Elige únicamente los puertos que ofrecen una interfaz web. ProxMenux no registra estos contenedores como aplicaciones LXC independientes.",
"dockerPublishedAdd": "Agregar enlace",
"dockerPublishedPort": "puerto {containerPort} del contenedor → {hostPort} en el LXC",
"noWebPorts": "No hay puertos web escuchando. Utilice Agregar puerto si aún desea crear un enlace manualmente.",
"trackUpstream": "Seguir versión disponible (opcional)",
"trackOff": "Desactivado: solo enlace",
@@ -1419,7 +1518,7 @@
"checkButton": "Controlar",
"editFieldsButton": "Editar campos",
"alsoDetectedContainer": "También detectado en este contenedor.",
"addAnotherApplication": "Agregar otra aplicación",
"addAnotherApplication": "Registrar otra aplicación",
"doneButton": "Hecho",
"editButton": "Editar",
"notificationsEnabled": "Notificaciones de actualización activas — clic para silenciar",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "Système d'exploitation + application",
"lastRun": "Dernière exécution : {date}",
"runSuccess": "✓ succès",
"runPartial": "partiellement terminée",
"runFailed": "✗ échoué",
"runDeferred": "reportée par sécurité",
"runSkipped": "rien en attente",
"releaseDelaySummary": "délai de {days} jour(s)",
"releaseDelayLabel": "Attendre après une nouvelle version d’application",
"releaseDelayNone": "Aucun délai d’attente",
"releaseDelayDays": "Attendre {days} jour(s)",
"releaseDelayHelp": "S’applique uniquement aux applications sélectionnées dont les versions sont suivies. Leurs versions en attente doivent avoir cet âge ; les applications sans suivi exécutent leur programme de mise à jour à chaque programmation.",
"notScheduled": "Non programmé",
"frequency": "Fréquence",
"cronExpression": "Expression Cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Packages de système d'exploitation uniquement",
"targetOptionApp": "Candidature uniquement",
"targetOptionBoth": "Système d'exploitation + application",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Conteneur Docker · {container}",
"dockerEngineTarget": "Moteur Docker",
"noTargets": "Aucune cible sélectionnée",
"selectAtLeastOne": "Sélectionnez au moins une cible de mise à jour.",
"deleteButton": "Supprimer le planning",
"deleteConfirm": "Supprimer les mises à jour planifiées pour ce conteneur ? Les valeurs par défaut appliquées (sauvegarde + redémarrage) sont conservées."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "Mises à jour du système d'exploitation + {appName}",
"osPlusApps": "Mises à jour du système d'exploitation et des applications",
"noUpdateMethodTitle": "Aucune méthode de mise à jour disponible",
"noUpdateMethodBody": "Aucune méthode de mise à jour disponible. Mettez à jour depuis l'application elle-même ou définissez une commande personnalisée dans l'onglet Application.",
"noUpdateMethodBody": "Aucune méthode de mise à jour n’a été identifiée pour cette application. Ajoutez une commande de mise à jour personnalisée.",
"ociImmutableTitle": "Conteneur d'images OCI",
"ociImmutableBody": "Les packages du système d'exploitation sont intégrés au moment de la création de l'image et ne peuvent pas être mis à jour sur place. Appliquez les mises à jour manuellement ou réinstallez avec une image plus récente.",
"hideNoticeButton": "Masquer cet avis pour cette application",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Application",
"installedLabel": "installé",
"upToDateAtLabel": "À jour à",
"versionLabel": "version {version}",
"versionTrackingPending": "Suivi des versions en attente : la prochaine vérification programmée indiquera les versions installée et disponible.",
"customCommandTitle": "Commande de mise à jour personnalisée",
"customCommandBody": "Exécutez une commande shell définie par l'utilisateur pour mettre à jour cette application dans le conteneur.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Masquer cet avis pour {appName}",
"detectedByPrefix": "Détecté par",
"editApp": "Modifier",
"configureUpdater": "Configurer",
"alsoDetectedTitle": "Également détecté dans ce conteneur :",
"detectedInline": "· détecté par {method}",
"customCommandLabel": "Commande de mise à jour personnalisée",
@@ -1245,9 +1260,9 @@
"removeButton": "Retirer",
"cancelButton": "Annuler",
"saveButton": "Sauvegarder",
"editCommandButton": "Modifier la commande",
"wireUpCommandButton": "Ajouter une commande de mise à jour personnalisée",
"versionTrackingPendingShort": "Suivi de version en attente.",
"versionTrackingNotConfigured": "Le suivi des versions n’est pas configuré.",
"managedByOsPackages": "Cette application est mise à jour par l’action sur les paquets du système.",
"noMethodBody": "Aucune méthode de mise à jour disponible. Mettez à jour à partir de l'application elle-même ou connectez une commande personnalisée que ProxMenux exécutera à l'intérieur du conteneur.",
"noMethodHideBody": "Cette application n'a aucune méthode de mise à jour enregistrée. Définissez-en un dans l’onglet Application ou ignorez cet avis.",
"hideForApp": "Masquer l'avis pour {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Aucune information de mise à jour pour l'instant - vérifiez depuis Sécurité → Secure Gateway.",
"ociTitle": "Conteneur d'images OCI",
"ociBody": "Ce conteneur a été créé à partir d'une image OCI (Docker). La gestion des mises à jour pour les conteneurs OCI est fournie avec la prochaine fonctionnalité d'installation OCI : les mises à jour reconstruiront le conteneur à partir d'une balise d'image plus récente plutôt que de corriger les packages à l'intérieur.",
"helperNotUpdateable": "Le registre des scripts de communauté marque cette application comme non modifiable.",
"helperDetectedTitle": "Détection d'un programme de mise à jour des scripts d'assistance",
"helperDetectedBody": "L’application manuelle est la plus sûre.",
"dockerImagesTitle": "Images Docker",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
"dockerImagesReadOnly": "Les tags sont comparés par digest de registre. Les services Compose peuvent télécharger l’image et être recréés depuis leur projet déclaré.",
"dockerEngineManagedByOs": "Docker Engine {version} est géré par paquets et mis à jour par l’action des paquets du SE ci-dessus.",
"dockerEngineSubheading": "Moteur",
"dockerEngineDetected": "Docker Engine {version} détecté",
"dockerEngineUpdateHelp": "Met à jour uniquement les paquets Docker installés et les dépendances nécessaires. Les autres paquets du système et les conteneurs ne sont pas modifiés.",
"updateDockerEngineOnly": "Mettre à jour Docker Engine",
"adguardWebUpdateOnly": "Cette installation d’AdGuard Home se met à jour depuis sa propre interface web.",
"openAdguard": "Ouvrir AdGuard Home",
"updateDockerImage": "Mettre à jour l’image",
"recreateStandaloneContainer": "Mettre à jour {container}",
"standaloneRecreatePending": "Les conteneurs autonomes nécessitent une recréation protégée : {names}",
"noDockerImages": "Docker est installé, mais aucune image taguée n’a été trouvée.",
"dockerInventoryStarting": "En attente de la fin du démarrage de Docker…",
"dockerInventoryUnavailable": "L’inventaire Docker n’a pas pu être lu. Vérifiez à nouveau lorsque Docker sera disponible.",
"usedByContainers": "Utilisée par : {names}",
"imageUpdateAvailable": "Nouvelle image disponible",
"imageUpToDate": "À jour",
"imageInstalledTag": "tag installé",
"imageDigestUnknown": "Digest indisponible",
"dockerPendingSummary": "{count} mise(s) à jour d’image Docker détectée(s). Utilisez le flux Docker ou Compose correspondant.",
"postApplyChecking": "Vérification du résultat de la mise à jour…",
"postApplyAllOk": "{count} package(s) appliqué(s) avec succès – rien en attente.",
"postApplyNothingPending": "Rien en attente – tout est à jour.",
"postApplyPartial": "{pending} package(s) toujours en attente après l'exécution.",
"postApplyPartialSubline": "{applied} appliqué.Certaines mises à jour n'ont pas été terminées – consultez la sortie du terminal ci-dessus."
},
"bulkUpdate": {
"title": "Mise à jour groupée",
"description": "Configurez une seule action pour mettre à jour le système et les méthodes sélectionnées dans ce conteneur.",
"loading": "Chargement de la configuration…",
"notConfigured": "Non configurée.",
"configure": "Configurer",
"edit": "Modifier",
"apply": "Appliquer les mises à jour",
"osTarget": "Système",
"osRequired": "Obligatoire pour une mise à jour groupée.",
"includesDependencies": "Inclut aussi les dépendances déclarées : {names}",
"noMethod": "Aucune méthode de mise à jour exécutable n’est disponible.",
"staleTarget": "Cette sélection n’est plus disponible. Retirez-la avant d’enregistrer.",
"dockerInventoryPending": "En attente de l’inventaire des images Docker…",
"missingDockerTarget": "Image Docker indisponible",
"selectAtLeastOne": "Sélectionnez au moins une cible de mise à jour en plus du système.",
"cancel": "Annuler",
"save": "Enregistrer",
"delete": "Supprimer la configuration",
"deleteConfirm": "Supprimer cette configuration de mise à jour groupée ?",
"saveFailed": "Impossible d’enregistrer la configuration de mise à jour groupée.",
"deleteFailed": "Impossible de supprimer la configuration de mise à jour groupée.",
"planFailed": "Impossible de préparer la mise à jour groupée. Vérifiez les méthodes indisponibles."
},
"appEditor": {
"closePanel": "Fermer le panneau",
"cancelButton": "Annuler",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Actuellement masqué — réapparaîtra dans la liste de détection",
"registerDifferent": "Enregistrez une autre application",
"detectedInContainer": "Détecté sur ce conteneur",
"versionDetected": "Version {version} détectée dans ce conteneur",
"legacyDetectorUsed": "Détectée avec une solution de compatibilité ancienne",
"dockerImagesTitle": "Mises à jour des images Docker",
"dockerImagesHelp": "Suit chaque image locale séparément du moteur Docker en comparant les digests immuables du registre.",
"refreshDockerImages": "Vérifier maintenant",
"noDockerImages": "Aucune image Docker taguée trouvée",
"usedByContainers": "Utilisée par : {names}",
"imageUpdateAvailable": "Nouvelle image disponible",
"imageUpToDate": "À jour",
"imageDigestUnknown": "Digest indisponible",
"dockerReadOnlyNote": "Vérification en lecture seule : aucune image téléchargée et aucun conteneur redémarré.",
"installedManaged": "Installé et géré par ProxMenux",
"nameLabel": "Nom",
"installedViaLabel": "Installé via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "par exemple, data.version ou releases[0].tag_name",
"dockerImageLabel": "Image DockerHub",
"dockerImagePlaceholder": "par exemple, linuxserver/plex ou nginx",
"dockerVersionedTagsHelp": "À utiliser uniquement pour les tags versionnés. Pour latest, stable ou lts, utilisez les mises à jour d’images Docker par digest.",
"dockerPresetSemver": "SemVer uniquement",
"dockerPresetSemverSuffix": "SemVer + suffixe",
"dockerTagFilterHelp": "Filtre les tags réels avant que ProxMenux choisisse la version la plus élevée.",
"dockerTagPreviewLabel": "Aperçu des tags réels",
"dockerTagPreviewCount": "{matched} correspondances sur {scanned}",
"dockerTagPreviewLoading": "Interrogation de Docker Hub…",
"dockerTagPreviewFailed": "Impossible d’afficher les tags Docker Hub",
"dockerTagPreviewEmpty": "Aucun tag réel ne correspond à ce filtre.",
"dockerMovingTag": "tag mobile",
"dockerMovingTagHelp": "Les tags mobiles ne contiennent pas de version. Suivez-les plutôt par digest dans les mises à jour d’images Docker.",
"tagRegexLabel": "Tag regex (avec groupe de capture)",
"tagRegexPlaceholder": "par exemple, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Enregistrez une application personnalisée",
"noAppsTitle": "Aucune candidature enregistrée",
"noAppsBody": "Enregistrez les applications exécutées dans ce conteneur pour obtenir des liens web et, si vous le souhaitez, le suivi des versions disponibles avec des notifications de nouvelles versions.",
"registerApplication": "Enregistrer une demande",
"registerApplication": "Enregistrer l’application",
"searchApplications": "Rechercher des applications",
"searchingApplications": "Recherche d’applications…",
"noApplicationsDetected": "Aucune application détectée.",
"noNewApplicationsDetected": "Aucune nouvelle application détectée.",
"oneNewApplicationDetected": "Une nouvelle application détectée.",
"newApplicationsDetected": "{count} nouvelles applications détectées.",
"detectionFailed": "Échec de la détection des applications",
"hiddenSuffix": "{count} masqué",
"confirmHide": "Masquer définitivement la détection « {name} » sur ce conteneur ?\n\nPour la récupérer plus tard, cliquez sur « Enregistrer une autre application » : la liste masquée y apparaît avec une option de restauration.",
"saveFailed": "Échec de l'enregistrement",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana a besoin",
"loadFailed": "Impossible de charger la configuration de l'application",
"checkFailed": "La vérification a échoué",
"testDetectorButton": "Tester le détecteur",
"testingDetectorButton": "Test du détecteur…",
"detectorTestTitle": "Résultat du test du détecteur",
"detectorTestNotSaved": "Brouillon uniquement · non enregistré",
"detectorTestNoVersion": "Aucune version détectée",
"detectorTestNoUpstream": "Aucune source amont configurée",
"detectorTestFailed": "Échec du test du détecteur",
"deleteFailed": "Échec de la suppression",
"dismissFailed": "Impossible d'ignorer la détection",
"restoreFailed": "Impossible de restaurer la détection",
@@ -1399,6 +1494,10 @@
"webLinks": "Liens Internet",
"addPort": "Ajouter un port",
"detectedPorts": "Ports détectés dans le conteneur — cliquez pour ajouter :",
"dockerPublishedServicesTitle": "Services Docker avec des ports publiés",
"dockerPublishedServicesHelp": "Sélectionnez uniquement les ports qui fournissent une interface web. ProxMenux n’enregistre pas ces conteneurs comme des applications LXC indépendantes.",
"dockerPublishedAdd": "Ajouter le lien",
"dockerPublishedPort": "port {containerPort} du conteneur → {hostPort} dans le LXC",
"noWebPorts": "Aucun port Web n'écoute. Utilisez Ajouter un port si vous souhaitez toujours créer un lien manuellement.",
"trackUpstream": "Suivre la version disponible (facultatif)",
"trackOff": "Désactivé : lien uniquement",
@@ -1416,7 +1515,7 @@
"checkButton": "Vérifier",
"editFieldsButton": "Modifier les champs",
"alsoDetectedContainer": "Également détecté sur ce conteneur",
"addAnotherApplication": "Ajouter une autre application",
"addAnotherApplication": "Enregistrer une autre application",
"doneButton": "Fait",
"editButton": "Modifier",
"upstreamErrorTimeout": "expiration du délai d'attente du réseau lors du contact en amont",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "Sistema operativo + applicazione",
"lastRun": "Ultima esecuzione: {date}",
"runSuccess": "✓ successo",
"runPartial": "completato parzialmente",
"runFailed": "✗ fallito",
"runDeferred": "rinviato per sicurezza",
"runSkipped": "nulla in sospeso",
"releaseDelaySummary": "attesa di {days} giorno/i",
"releaseDelayLabel": "Attendi dopo una nuova versione dell’app",
"releaseDelayNone": "Nessun periodo di attesa",
"releaseDelayDays": "Attendi {days} giorno/i",
"releaseDelayHelp": "Si applica solo alle app selezionate con monitoraggio della versione. Le versioni in sospeso devono avere questa età; le app senza monitoraggio eseguono l’aggiornamento a ogni pianificazione.",
"notScheduled": "Non programmato",
"frequency": "Frequenza",
"cronExpression": "Espressione cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Solo pacchetti del sistema operativo",
"targetOptionApp": "Solo applicazione",
"targetOptionBoth": "Sistema operativo + applicazione",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Container Docker · {container}",
"dockerEngineTarget": "Motore Docker",
"noTargets": "Nessun obiettivo selezionato",
"selectAtLeastOne": "Seleziona almeno un obiettivo di aggiornamento.",
"deleteButton": "Elimina pianificazione",
"deleteConfirm": "Rimuovere gli aggiornamenti pianificati per questo contenitore? Le impostazioni predefinite di applicazione (backup + riavvio) vengono mantenute."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "Sistema operativo + {appName} aggiornamenti",
"osPlusApps": "Aggiornamenti del sistema operativo e delle app",
"noUpdateMethodTitle": "Nessun metodo di aggiornamento disponibile",
"noUpdateMethodBody": "Nessun metodo di aggiornamento disponibile. Aggiorna dall'app stessa o imposta un comando personalizzato nella scheda App.",
"noUpdateMethodBody": "Non è stato identificato alcun metodo di aggiornamento per questa applicazione. Aggiungi un comando di aggiornamento personalizzato.",
"ociImmutableTitle": "Contenitore di immagini OCI",
"ociImmutableBody": "I pacchetti del sistema operativo vengono integrati in fase di creazione dell'immagine e non possono essere aggiornati sul posto. Applica gli aggiornamenti manualmente o reinstallali con un'immagine più recente.",
"hideNoticeButton": "Nascondi questo avviso per questa app",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Applicazione",
"installedLabel": "installato",
"upToDateAtLabel": "Aggiornato a",
"versionLabel": "versione {version}",
"versionTrackingPending": "Monitoraggio versioni in attesa: il prossimo controllo pianificato mostrerà la versione installata e quella disponibile.",
"customCommandTitle": "Comando di aggiornamento personalizzato",
"customCommandBody": "Esegui un comando shell definito dall'utente per aggiornare questa app all'interno del contenitore.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Nascondi questo avviso per {appName}",
"detectedByPrefix": "Rilevato da",
"editApp": "Modificare",
"configureUpdater": "Configura",
"alsoDetectedTitle": "Rilevato anche in questo contenitore:",
"detectedInline": "· rilevato da {method}",
"customCommandLabel": "Comando di aggiornamento personalizzato",
@@ -1245,9 +1260,9 @@
"removeButton": "Rimuovere",
"cancelButton": "Cancellare",
"saveButton": "Salva",
"editCommandButton": "Modifica comando",
"wireUpCommandButton": "Aggiungi comando di aggiornamento personalizzato",
"versionTrackingPendingShort": "Monitoraggio della versione in sospeso.",
"versionTrackingNotConfigured": "Il monitoraggio della versione non è configurato.",
"managedByOsPackages": "Questa applicazione viene aggiornata tramite l’azione dei pacchetti del sistema operativo.",
"noMethodBody": "Nessun metodo di aggiornamento disponibile. Aggiorna dall'app stessa o collega un comando personalizzato che ProxMenux eseguirà all'interno del contenitore.",
"noMethodHideBody": "Per questa app non è registrato alcun metodo di aggiornamento. Impostane uno dalla scheda App o ignora questo avviso.",
"hideForApp": "Nascondi avviso per {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Nessuna informazione di aggiornamento ancora: controlla da Sicurezza → Secure Gateway.",
"ociTitle": "Contenitore di immagini OCI",
"ociBody": "Questo contenitore è stato creato da un'immagine OCI (Docker). La gestione degli aggiornamenti per i contenitori OCI arriverà con la prossima funzionalità di installazione OCI: gli aggiornamenti ricostruiranno il contenitore da un tag immagine più recente anziché applicare patch ai pacchetti all'interno.",
"helperNotUpdateable": "Il registro degli script della community contrassegna questa app come non aggiornabile.",
"helperDetectedTitle": "Rilevato un aggiornamento degli script helper",
"helperDetectedBody": "L'applicazione manuale è più sicura.",
"dockerImagesTitle": "Immagini Docker",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Immagini",
"dockerImagesReadOnly": "I tag vengono confrontati tramite digest del registro. I servizi Compose possono scaricare l’immagine ed essere ricreati dal progetto dichiarato.",
"dockerEngineManagedByOs": "Docker Engine {version} è gestito dai pacchetti e viene aggiornato dall’azione dei pacchetti del sistema operativo qui sopra.",
"dockerEngineSubheading": "Motore",
"dockerEngineDetected": "Docker Engine {version} rilevato",
"dockerEngineUpdateHelp": "Aggiorna solo i pacchetti Docker installati e le dipendenze necessarie. Non aggiorna altri pacchetti del sistema operativo né ricrea i container.",
"updateDockerEngineOnly": "Aggiorna Docker Engine",
"adguardWebUpdateOnly": "Questa installazione di AdGuard Home si aggiorna dalla propria interfaccia web.",
"openAdguard": "Apri AdGuard Home",
"updateDockerImage": "Aggiorna immagine",
"recreateStandaloneContainer": "Aggiorna {container}",
"standaloneRecreatePending": "I container indipendenti richiedono una ricreazione protetta: {names}",
"noDockerImages": "Docker è installato, ma non sono state trovate immagini con tag.",
"dockerInventoryStarting": "In attesa che Docker completi l’avvio…",
"dockerInventoryUnavailable": "Non è stato possibile leggere l’inventario Docker. Controlla di nuovo quando Docker è disponibile.",
"usedByContainers": "Usata da: {names}",
"imageUpdateAvailable": "Nuova immagine disponibile",
"imageUpToDate": "Aggiornata",
"imageInstalledTag": "tag installato",
"imageDigestUnknown": "Digest non disponibile",
"dockerPendingSummary": "Rilevati {count} aggiornamenti di immagini Docker. Usa il relativo flusso Docker o Compose.",
"postApplyChecking": "Verifica del risultato dell'aggiornamento…",
"postApplyAllOk": "pacchetto/i {count} applicato correttamente: nulla in sospeso.",
"postApplyNothingPending": "nulla in sospeso: tutto è aggiornato.",
"postApplyPartial": "{pending} pacchetto/i ancora in sospeso dopo l'esecuzione.",
"postApplyPartialSubline": "{applied} applicato.Alcuni aggiornamenti non sono stati completati: esamina l'output del terminale sopra."
},
"bulkUpdate": {
"title": "Aggiornamento in blocco",
"description": "Configura una sola azione per aggiornare il sistema operativo e i metodi selezionati in questo container.",
"loading": "Caricamento configurazione…",
"notConfigured": "Non configurato.",
"configure": "Configura",
"edit": "Modifica",
"apply": "Applica aggiornamenti",
"osTarget": "Sistema operativo",
"osRequired": "Obbligatorio per un aggiornamento in blocco.",
"includesDependencies": "Include anche le dipendenze dichiarate: {names}",
"noMethod": "Non è disponibile alcun metodo di aggiornamento eseguibile.",
"staleTarget": "Questa selezione non è più disponibile. Rimuovila prima di salvare.",
"dockerInventoryPending": "In attesa dell’inventario delle immagini Docker…",
"missingDockerTarget": "Immagine Docker non disponibile",
"selectAtLeastOne": "Seleziona almeno una destinazione di aggiornamento oltre al sistema operativo.",
"cancel": "Annulla",
"save": "Salva",
"delete": "Rimuovi configurazione",
"deleteConfirm": "Rimuovere questa configurazione di aggiornamento in blocco?",
"saveFailed": "Impossibile salvare la configurazione di aggiornamento in blocco.",
"deleteFailed": "Impossibile rimuovere la configurazione di aggiornamento in blocco.",
"planFailed": "Impossibile preparare l’aggiornamento in blocco. Controlla i metodi non disponibili."
},
"appEditor": {
"closePanel": "Chiudi pannello",
"cancelButton": "Cancellare",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Attualmente nascosto: riapparirà nell'elenco di rilevamento",
"registerDifferent": "Registra un'altra app",
"detectedInContainer": "Rilevato su questo contenitore",
"versionDetected": "Versione {version} rilevata in questo container",
"legacyDetectorUsed": "Rilevata con un fallback di compatibilità legacy",
"dockerImagesTitle": "Aggiornamenti immagini Docker",
"dockerImagesHelp": "Controlla ogni immagine locale separatamente dal motore Docker confrontando i digest immutabili del registro.",
"refreshDockerImages": "Controlla ora",
"noDockerImages": "Nessuna immagine Docker con tag trovata",
"usedByContainers": "Usata da: {names}",
"imageUpdateAvailable": "Nuova immagine disponibile",
"imageUpToDate": "Aggiornata",
"imageDigestUnknown": "Digest non disponibile",
"dockerReadOnlyNote": "Controllo in sola lettura: nessuna immagine viene scaricata e nessun container viene riavviato.",
"installedManaged": "Installato e gestito da ProxMenux",
"nameLabel": "Nome",
"installedViaLabel": "Installato tramite",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "ad esempio, data.version o releases[0].tag_name",
"dockerImageLabel": "Immagine dell'hub Docker",
"dockerImagePlaceholder": "ad esempio, linuxserver/plex o nginx",
"dockerVersionedTagsHelp": "Usa questa opzione solo per tag con versione. Per latest, stable o lts usa gli aggiornamenti immagini Docker basati sul digest.",
"dockerPresetSemver": "Solo SemVer",
"dockerPresetSemverSuffix": "SemVer + suffisso",
"dockerTagFilterHelp": "Filtra i tag reali prima che ProxMenux scelga la versione più alta.",
"dockerTagPreviewLabel": "Anteprima tag reali",
"dockerTagPreviewCount": "{matched} corrispondenze su {scanned}",
"dockerTagPreviewLoading": "Controllo di Docker Hub…",
"dockerTagPreviewFailed": "Impossibile mostrare i tag di Docker Hub",
"dockerTagPreviewEmpty": "Nessun tag reale corrisponde a questo filtro.",
"dockerMovingTag": "tag mobile",
"dockerMovingTagHelp": "I tag mobili non contengono una versione. Tracciali tramite digest negli aggiornamenti immagini Docker.",
"tagRegexLabel": "Tag regex (con gruppo di acquisizione)",
"tagRegexPlaceholder": "ad esempio, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registra un'app personalizzata",
"noAppsTitle": "Nessuna domanda registrata",
"noAppsBody": "Registra le applicazioni in esecuzione nel contenitore per ottenere collegamenti web e, facoltativamente, il monitoraggio delle versioni disponibili con notifiche delle nuove release.",
"registerApplication": "Registra l'applicazione",
"registerApplication": "Registra applicazione",
"searchApplications": "Cerca applicazioni",
"searchingApplications": "Ricerca applicazioni…",
"noApplicationsDetected": "Nessuna applicazione rilevata.",
"noNewApplicationsDetected": "Nessuna nuova applicazione rilevata.",
"oneNewApplicationDetected": "Rilevata una nuova applicazione.",
"newApplicationsDetected": "Rilevate {count} nuove applicazioni.",
"detectionFailed": "Rilevamento delle applicazioni non riuscito",
"hiddenSuffix": "{count} nascosto",
"confirmHide": "Nascondere permanentemente il rilevamento \"{name}\" su questo contenitore?\n\nPer ripristinarlo in un secondo momento, fai clic su \"Registra un'altra app\": l'elenco nascosto viene visualizzato lì con un'opzione di ripristino.",
"saveFailed": "Salvataggio non riuscito",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana ha bisogno",
"loadFailed": "Impossibile caricare la configurazione dell'app",
"checkFailed": "Controllo fallito",
"testDetectorButton": "Prova rilevatore",
"testingDetectorButton": "Prova del rilevatore…",
"detectorTestTitle": "Risultato della prova del rilevatore",
"detectorTestNotSaved": "Solo bozza · non salvata",
"detectorTestNoVersion": "Nessuna versione rilevata",
"detectorTestNoUpstream": "Nessuna fonte upstream configurata",
"detectorTestFailed": "Prova del rilevatore non riuscita",
"deleteFailed": "Eliminazione non riuscita",
"dismissFailed": "Impossibile ignorare il rilevamento",
"restoreFailed": "Impossibile ripristinare il rilevamento",
@@ -1399,6 +1494,10 @@
"webLinks": "Collegamenti Web",
"addPort": "Aggiungi porto",
"detectedPorts": "Porte rilevate nel contenitore: fai clic per aggiungere:",
"dockerPublishedServicesTitle": "Servizi Docker con porte pubblicate",
"dockerPublishedServicesHelp": "Seleziona solo le porte che forniscono un’interfaccia web. ProxMenux non registra questi container come applicazioni LXC indipendenti.",
"dockerPublishedAdd": "Aggiungi link",
"dockerPublishedPort": "porta {containerPort} del container → {hostPort} nell’LXC",
"noWebPorts": "Nessuna porta web in ascolto. Utilizza Aggiungi porta se desideri comunque creare un collegamento manualmente.",
"trackUpstream": "Monitora la versione disponibile (facoltativo)",
"trackOff": "Disattivato: solo collegamento",
@@ -1416,7 +1515,7 @@
"checkButton": "Controllo",
"editFieldsButton": "Modifica campi",
"alsoDetectedContainer": "Rilevato anche su questo contenitore",
"addAnotherApplication": "Aggiungi un'altra applicazione",
"addAnotherApplication": "Registra un'altra applicazione",
"doneButton": "Fatto",
"editButton": "Modificare",
"upstreamErrorTimeout": "timeout della rete durante il contatto a monte",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "SO + aplicativo",
"lastRun": "Última execução: {date}",
"runSuccess": "✓ sucesso",
"runPartial": "concluída parcialmente",
"runFailed": "✗ falhou",
"runDeferred": "adiada por segurança",
"runSkipped": "nada pendente",
"releaseDelaySummary": "espera de {days} dia(s)",
"releaseDelayLabel": "Esperar após uma nova versão da app",
"releaseDelayNone": "Sem período de espera",
"releaseDelayDays": "Esperar {days} dia(s)",
"releaseDelayHelp": "Aplica-se apenas às apps selecionadas com acompanhamento de versões. As versões pendentes devem ter esta idade; as apps sem acompanhamento executam o atualizador em cada agendamento.",
"notScheduled": "Não agendado",
"frequency": "Freqüência",
"cronExpression": "Expressão Cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Somente pacotes de sistema operacional",
"targetOptionApp": "Somente aplicativo",
"targetOptionBoth": "SO + aplicativo",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Contentor Docker · {container}",
"dockerEngineTarget": "Motor Docker",
"noTargets": "Nenhum alvo selecionado",
"selectAtLeastOne": "Selecione pelo menos um alvo de atualização.",
"deleteButton": "Excluir programação",
"deleteConfirm": "Remover as atualizações agendadas para este contêiner? Os padrões de aplicação (backup + reinicialização) são mantidos."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "SO + atualizações de {appName}",
"osPlusApps": "Atualizações do sistema operacional + aplicativos",
"noUpdateMethodTitle": "Nenhum método de atualização disponível",
"noUpdateMethodBody": "Nenhum método de atualização disponível. Atualize a partir do próprio aplicativo ou defina um comando personalizado na guia Aplicativo.",
"noUpdateMethodBody": "Não foi identificado nenhum método de atualização para esta aplicação. Adicione um comando de atualização personalizado.",
"ociImmutableTitle": "Contêiner de imagem OCI",
"ociImmutableBody": "Os pacotes do sistema operacional são incorporados no momento da criação da imagem e não podem ser atualizados no local. Aplique as atualizações manualmente ou reinstale com uma imagem mais recente.",
"hideNoticeButton": "Ocultar este aviso para este app",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Aplicativo",
"installedLabel": "instalado",
"upToDateAtLabel": "Atualizado em",
"versionLabel": "versão {version}",
"versionTrackingPending": "Rastreamento de versões pendente — a próxima verificação agendada mostrará as versões instalada e disponível.",
"customCommandTitle": "Comando de atualização personalizado",
"customCommandBody": "Execute um comando shell definido pelo usuário para atualizar este aplicativo dentro do contêiner.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Ocultar este aviso para {appName}",
"detectedByPrefix": "Detectado por",
"editApp": "Editar",
"configureUpdater": "Configurar",
"alsoDetectedTitle": "Também detectado neste contêiner:",
"detectedInline": "· detectado por {method}",
"customCommandLabel": "Comando de atualização personalizado",
@@ -1245,9 +1260,9 @@
"removeButton": "Remover",
"cancelButton": "Cancelar",
"saveButton": "Salvar",
"editCommandButton": "Editar comando",
"wireUpCommandButton": "Adicionar comando de atualização personalizado",
"versionTrackingPendingShort": "Rastreamento de versão pendente.",
"versionTrackingNotConfigured": "O acompanhamento de versões não está configurado.",
"managedByOsPackages": "Esta aplicação é atualizada através da ação de pacotes do SO.",
"noMethodBody": "Nenhum método de atualização disponível. Atualize a partir do próprio aplicativo ou conecte um comando personalizado que o ProxMenux executará dentro do contêiner.",
"noMethodHideBody": "Este aplicativo não possui nenhum método de atualização registrado. Defina um na guia Aplicativo ou ignore este aviso.",
"hideForApp": "Ocultar aviso para {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Nenhuma informação de atualização ainda — verifique em Segurança → Secure Gateway.",
"ociTitle": "Contêiner de imagem OCI",
"ociBody": "Este contêiner foi criado a partir de uma imagem OCI (Docker). O gerenciamento de atualizações para contêineres OCI vem com o próximo recurso de instalação do OCI – as atualizações reconstruirão o contêiner a partir de uma tag de imagem mais recente, em vez de corrigir os pacotes internos.",
"helperNotUpdateable": "O registro de scripts da comunidade marca este aplicativo como não atualizável.",
"helperDetectedTitle": "Detectou um atualizador de scripts auxiliares",
"helperDetectedBody": "Aplicar manualmente é mais seguro.",
"dockerImagesTitle": "Imagens Docker",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Imagens",
"dockerImagesReadOnly": "As tags são comparadas pelo digest do registo. Os serviços Compose podem descarregar a imagem e ser recriados a partir do projeto declarado.",
"dockerEngineManagedByOs": "O Docker Engine {version} é gerido por pacotes e atualizado pela ação de pacotes do SO acima.",
"dockerEngineSubheading": "Motor",
"dockerEngineDetected": "Docker Engine {version} detetado",
"dockerEngineUpdateHelp": "Atualiza apenas os pacotes Docker instalados e as dependências necessárias. Não atualiza outros pacotes do SO nem recria contentores.",
"updateDockerEngineOnly": "Atualizar Docker Engine",
"adguardWebUpdateOnly": "Esta instalação do AdGuard Home é atualizada na sua própria interface web.",
"openAdguard": "Abrir AdGuard Home",
"updateDockerImage": "Atualizar imagem",
"recreateStandaloneContainer": "Atualizar {container}",
"standaloneRecreatePending": "Os contentores independentes requerem uma recriação protegida: {names}",
"noDockerImages": "O Docker está instalado, mas não foram encontradas imagens com tag.",
"dockerInventoryStarting": "A aguardar que o Docker termine de iniciar…",
"dockerInventoryUnavailable": "Não foi possível ler o inventário do Docker. Verifique novamente quando o Docker estiver disponível.",
"usedByContainers": "Usada por: {names}",
"imageUpdateAvailable": "Nova imagem disponível",
"imageUpToDate": "Atualizada",
"imageInstalledTag": "etiqueta instalada",
"imageDigestUnknown": "Digest indisponível",
"dockerPendingSummary": "Foram detetadas {count} atualizações de imagens Docker. Use o respetivo fluxo Docker ou Compose.",
"postApplyChecking": "Verificando o resultado da atualização…",
"postApplyAllOk": "{count} pacote(s) aplicado(s) com sucesso — nada pendente.",
"postApplyNothingPending": "Nada pendente — tudo está atualizado.",
"postApplyPartial": "{pending} pacote(s) ainda pendente(s) após a execução.",
"postApplyPartialSubline": "{applied} aplicado.Algumas atualizações não foram concluídas – revise a saída do terminal acima."
},
"bulkUpdate": {
"title": "Atualização em bloco",
"description": "Configure uma única ação para atualizar o sistema operativo e os métodos selecionados neste contentor.",
"loading": "A carregar configuração…",
"notConfigured": "Não configurada.",
"configure": "Configurar",
"edit": "Editar",
"apply": "Aplicar atualizações",
"osTarget": "Sistema operativo",
"osRequired": "Obrigatório numa atualização em bloco.",
"includesDependencies": "Inclui também as dependências declaradas: {names}",
"noMethod": "Não está disponível nenhum método de atualização executável.",
"staleTarget": "Esta seleção já não está disponível. Remova-a antes de guardar.",
"dockerInventoryPending": "A aguardar o inventário de imagens Docker…",
"missingDockerTarget": "Imagem Docker indisponível",
"selectAtLeastOne": "Selecione pelo menos um destino de atualização além do sistema operativo.",
"cancel": "Cancelar",
"save": "Guardar",
"delete": "Remover configuração",
"deleteConfirm": "Remover esta configuração de atualização em bloco?",
"saveFailed": "Não foi possível guardar a configuração de atualização em bloco.",
"deleteFailed": "Não foi possível remover a configuração de atualização em bloco.",
"planFailed": "Não foi possível preparar a atualização em bloco. Reveja os métodos indisponíveis."
},
"appEditor": {
"closePanel": "Fechar painel",
"cancelButton": "Cancelar",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Atualmente oculto — reaparecerá na lista de detecção",
"registerDifferent": "Registre um aplicativo diferente",
"detectedInContainer": "Detectado neste contêiner",
"versionDetected": "Versão {version} detetada neste contentor",
"legacyDetectorUsed": "Detetada com um fallback de compatibilidade antigo",
"dockerImagesTitle": "Atualizações de imagens Docker",
"dockerImagesHelp": "Verifica cada imagem local separadamente do motor Docker comparando digests imutáveis do registo.",
"refreshDockerImages": "Verificar agora",
"noDockerImages": "Não foram encontradas imagens Docker com tag",
"usedByContainers": "Usada por: {names}",
"imageUpdateAvailable": "Nova imagem disponível",
"imageUpToDate": "Atualizada",
"imageDigestUnknown": "Digest indisponível",
"dockerReadOnlyNote": "Verificação só de leitura: nenhuma imagem é descarregada e nenhum contentor é reiniciado.",
"installedManaged": "Instalado e gerenciado por ProxMenux",
"nameLabel": "Nome",
"installedViaLabel": "Instalado via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "por exemplo, data.version ou releases[0].tag_name",
"dockerImageLabel": "Imagem do DockerHub",
"dockerImagePlaceholder": "por exemplo, linuxserver/plex ou nginx",
"dockerVersionedTagsHelp": "Utilize apenas com tags versionadas. Para latest, stable ou lts, use as atualizações de imagens Docker por digest.",
"dockerPresetSemver": "Apenas SemVer",
"dockerPresetSemverSuffix": "SemVer + sufixo",
"dockerTagFilterHelp": "Filtra as tags reais antes de o ProxMenux escolher a versão mais alta.",
"dockerTagPreviewLabel": "Pré-visualização de tags reais",
"dockerTagPreviewCount": "{matched} correspondências de {scanned}",
"dockerTagPreviewLoading": "A consultar o Docker Hub…",
"dockerTagPreviewFailed": "Não foi possível mostrar as tags do Docker Hub",
"dockerTagPreviewEmpty": "Nenhuma tag real corresponde a este filtro.",
"dockerMovingTag": "tag móvel",
"dockerMovingTagHelp": "As tags móveis não contêm uma versão. Acompanhe-as pelo digest nas atualizações de imagens Docker.",
"tagRegexLabel": "Tag regex (com grupo de captura)",
"tagRegexPlaceholder": "por exemplo, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registre um aplicativo personalizado",
"noAppsTitle": "Nenhum aplicativo cadastrado",
"noAppsBody": "Registre os aplicativos executados neste contêiner para obter links da web e, opcionalmente, rastrear versões disponíveis com notificações de novos lançamentos.",
"registerApplication": "Registrar aplicativo",
"registerApplication": "Registar aplicação",
"searchApplications": "Procurar aplicações",
"searchingApplications": "A procurar aplicações…",
"noApplicationsDetected": "Nenhuma aplicação detetada.",
"noNewApplicationsDetected": "Nenhuma aplicação nova detetada.",
"oneNewApplicationDetected": "Foi detetada uma nova aplicação.",
"newApplicationsDetected": "Foram detetadas {count} aplicações novas.",
"detectionFailed": "Falha ao detetar aplicações",
"hiddenSuffix": "{count} oculto",
"confirmHide": "Ocultar permanentemente a detecção \"{name}\" neste contêiner?\n\nPara recuperá-lo mais tarde, clique em “Registrar um aplicativo diferente” – a lista oculta aparece com a opção Restaurar.",
"saveFailed": "Falha ao salvar",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Necessidades de grafana",
"loadFailed": "Não foi possível carregar a configuração do aplicativo",
"checkFailed": "Falha na verificação",
"testDetectorButton": "Testar detetor",
"testingDetectorButton": "A testar o detetor…",
"detectorTestTitle": "Resultado do teste do detetor",
"detectorTestNotSaved": "Apenas rascunho · não guardado",
"detectorTestNoVersion": "Nenhuma versão detetada",
"detectorTestNoUpstream": "Nenhuma fonte upstream configurada",
"detectorTestFailed": "Falha no teste do detetor",
"deleteFailed": "Falha na exclusão",
"dismissFailed": "Não foi possível descartar a detecção",
"restoreFailed": "Não foi possível restaurar a detecção",
@@ -1399,6 +1494,10 @@
"webLinks": "Links da web",
"addPort": "Adicionar porta",
"detectedPorts": "Portas detectadas no contêiner — clique para adicionar:",
"dockerPublishedServicesTitle": "Serviços Docker com portas publicadas",
"dockerPublishedServicesHelp": "Selecione apenas as portas que fornecem uma interface Web. O ProxMenux não regista estes contentores como aplicações LXC independentes.",
"dockerPublishedAdd": "Adicionar ligação",
"dockerPublishedPort": "porta {containerPort} do contentor → {hostPort} no LXC",
"noWebPorts": "Nenhuma porta da web escutando. Use Adicionar porta se ainda quiser criar um link manualmente.",
"trackUpstream": "Rastrear versão disponível (opcional)",
"trackOff": "Desativado – apenas link",
@@ -1416,7 +1515,7 @@
"checkButton": "Verificar",
"editFieldsButton": "Editar campos",
"alsoDetectedContainer": "Também detectado neste contêiner",
"addAnotherApplication": "Adicione outro aplicativo",
"addAnotherApplication": "Registar outra aplicação",
"doneButton": "Feito",
"editButton": "Editar",
"upstreamErrorTimeout": "Tempo limite da rede ao entrar em contato com o upstream",
+107 -8
View File
@@ -1162,7 +1162,15 @@
"targetBoth": "Systém + aplikácia",
"lastRun": "Posledné spustenie: {date}",
"runSuccess": "✓ úspešné",
"runPartial": "čiastočne dokončené",
"runFailed": "✗ zlyhalo",
"runDeferred": "odložené z bezpečnostných dôvodov",
"runSkipped": "nič nečaká",
"releaseDelaySummary": "odklad {days} deň/dní",
"releaseDelayLabel": "Počkať po novej verzii aplikácie",
"releaseDelayNone": "Bez čakacej doby",
"releaseDelayDays": "Počkať {days} deň/dní",
"releaseDelayHelp": "Platí iba pre vybrané aplikácie so sledovaním verzie. Ich čakajúce verzie musia dosiahnuť tento vek; aplikácie bez sledovania spustia aktualizátor pri každom naplánovanom termíne.",
"notScheduled": "Bez plánu",
"frequency": "Frekvencia",
"cronExpression": "Cron výraz",
@@ -1171,6 +1179,11 @@
"targetOptionOs": "Iba balíky systému",
"targetOptionApp": "Iba aplikáciu",
"targetOptionBoth": "Systém + aplikáciu",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Docker kontajner · {container}",
"dockerEngineTarget": "Docker Engine",
"noTargets": "Nie sú vybrané žiadne ciele",
"selectAtLeastOne": "Vyberte aspoň jeden cieľ aktualizácie.",
"deleteButton": "Odstrániť plán",
"deleteConfirm": "Odstrániť plánované aktualizácie tohto kontajnera? Predvolené nastavenia (záloha a reštart) zostanú zachované."
},
@@ -1216,7 +1229,7 @@
"osPlusApp": "Aktualizácie systému + {appName}",
"osPlusApps": "Aktualizácie systému + aplikácií",
"noUpdateMethodTitle": "Aktualizácia nie je nastavená",
"noUpdateMethodBody": "Pre túto aplikáciu nie je nastavený spôsob aktualizácie. Aktualizujte ju priamo v aplikácii alebo nastavte vlastný príkaz na karte Aplikácia.",
"noUpdateMethodBody": "Pre túto aplikáciu nebola identifikovaná žiadna metóda aktualizácie. Pridajte vlastný aktualizačný príkaz.",
"ociImmutableTitle": "Kontajner z OCI obrazu",
"ociImmutableBody": "Balíky systému sú súčasťou obrazu a nemožno ich aktualizovať priamo. Aktualizujte ich ručne alebo kontajner preinštalujte z novšieho obrazu.",
"hideNoticeButton": "Skryť toto upozornenie pre aplikáciu",
@@ -1229,6 +1242,7 @@
"applicationDefaultName": "Aplikácia",
"installedLabel": "nainštalované",
"upToDateAtLabel": "Aktuálna verzia",
"versionLabel": "verzia {version}",
"versionTrackingPending": "Čaká sa na údaje o verzii — nainštalovaná a dostupná verzia sa doplnia pri najbližšej plánovanej kontrole.",
"customCommandTitle": "Vlastný príkaz na aktualizáciu",
"customCommandBody": "Spustí v kontajneri vlastný shell príkaz, ktorý aktualizuje túto aplikáciu.",
@@ -1237,6 +1251,7 @@
"hideNoticeAction": "Skryť upozornenie pre {appName}",
"detectedByPrefix": "Zistené cez",
"editApp": "Upraviť",
"configureUpdater": "Nastaviť",
"alsoDetectedTitle": "V tomto kontajneri sa našli aj:",
"detectedInline": "· zistené cez {method}",
"customCommandLabel": "Vlastný príkaz na aktualizáciu",
@@ -1244,9 +1259,9 @@
"removeButton": "Odstrániť",
"cancelButton": "Zrušiť",
"saveButton": "Uložiť",
"editCommandButton": "Upraviť príkaz",
"wireUpCommandButton": "Pridať vlastný príkaz na aktualizáciu",
"versionTrackingPendingShort": "Čaká sa na údaje o verzii.",
"versionTrackingNotConfigured": "Sledovanie verzie nie je nastavené.",
"managedByOsPackages": "Táto aplikácia sa aktualizuje prostredníctvom akcie balíkov operačného systému.",
"noMethodBody": "Pre túto aplikáciu nie je nastavený spôsob aktualizácie. Aktualizujte ju priamo v aplikácii alebo pridajte vlastný príkaz, ktorý ProxMenux spustí v kontajneri.",
"noMethodHideBody": "Táto aplikácia nemá nastavený spôsob aktualizácie. Nastavte ho na karte Aplikácia alebo toto upozornenie skryte.",
"hideForApp": "Skryť upozornenie pre {appName}",
@@ -1265,15 +1280,59 @@
"noManagedUpdateInfo": "Zatiaľ nie sú žiadne informácie o aktualizácii – skontrolujte ich v časti Zabezpečenie → Secure Gateway.",
"ociTitle": "Kontajner z OCI obrazu",
"ociBody": "Tento kontajner bol vytvorený z OCI (Docker) obrazu. Pripravovaná podpora aktualizácií OCI kontajnerov vytvorí kontajner znova z novšieho tagu obrazu namiesto aktualizácie balíkov vo vnútri.",
"helperNotUpdateable": "Register community-scripts označuje túto aplikáciu ako neaktualizovateľnú.",
"helperDetectedTitle": "Našiel sa aktualizačný skript z helper-scripts",
"helperDetectedBody": "Najbezpečnejšie je spustiť aktualizáciu ručne.",
"dockerImagesTitle": "Docker obrazy",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Obrazy",
"dockerImagesReadOnly": "Značky sa porovnávajú podľa digestu registra. Služby Compose môžu stiahnuť obraz a znova sa vytvoriť z deklarovaného projektu.",
"dockerEngineManagedByOs": "Docker Engine {version} je spravovaný balíkmi a aktualizuje sa vyššie uvedenou akciou balíkov OS.",
"dockerEngineSubheading": "Engine",
"dockerEngineDetected": "Zistený Docker Engine {version}",
"dockerEngineUpdateHelp": "Aktualizuje iba nainštalované balíky Docker a potrebné závislosti. Ostatné balíky OS ani kontajnery nemení.",
"updateDockerEngineOnly": "Aktualizovať Docker Engine",
"adguardWebUpdateOnly": "Táto inštalácia AdGuard Home sa aktualizuje vo vlastnom webovom rozhraní.",
"openAdguard": "Otvoriť AdGuard Home",
"updateDockerImage": "Aktualizovať obraz",
"recreateStandaloneContainer": "Aktualizovať {container}",
"standaloneRecreatePending": "Samostatné kontajnery vyžadujú chránené opätovné vytvorenie: {names}",
"noDockerImages": "Docker je nainštalovaný, ale nenašli sa žiadne označené obrazy.",
"dockerInventoryStarting": "Čaká sa na dokončenie spustenia Dockeru…",
"dockerInventoryUnavailable": "Inventár Dockeru sa nepodarilo načítať. Skontrolujte ho znova, keď bude Docker dostupný.",
"usedByContainers": "Používa: {names}",
"imageUpdateAvailable": "Dostupný nový obraz",
"imageUpToDate": "Aktuálne",
"imageInstalledTag": "nainštalovaná značka",
"imageDigestUnknown": "Digest nie je dostupný",
"dockerPendingSummary": "Zistené aktualizácie Docker obrazov: {count}. Použite príslušný postup Docker alebo Compose.",
"postApplyChecking": "Overuje sa výsledok aktualizácie…",
"postApplyAllOk": "{count} balíkov bolo úspešne použitých – nič sa nečaká.",
"postApplyNothingPending": "Nič sa nečaká – všetko je aktuálne.",
"postApplyPartial": "{pending} balíkov, ktoré po spustení stále čakajú.",
"postApplyPartialSubline": "Použilo sa {applied}. Niektoré aktualizácie sa nedokončili – skontrolujte výstup terminálu vyššie."
},
"bulkUpdate": {
"title": "Hromadná aktualizácia",
"description": "Nastavte jednu akciu na aktualizáciu operačného systému a vybraných metód v tomto kontajneri.",
"loading": "Načítava sa konfigurácia…",
"notConfigured": "Nenakonfigurované.",
"configure": "Nastaviť",
"edit": "Upraviť",
"apply": "Použiť aktualizácie",
"osTarget": "Operačný systém",
"osRequired": "Povinné pre hromadnú aktualizáciu.",
"includesDependencies": "Zahŕňa aj deklarované závislosti: {names}",
"noMethod": "Nie je dostupná žiadna spustiteľná metóda aktualizácie.",
"staleTarget": "Tento výber už nie je dostupný. Pred uložením ho odstráňte.",
"dockerInventoryPending": "Čaká sa na inventár obrazov Docker…",
"missingDockerTarget": "Obraz Docker nie je dostupný",
"selectAtLeastOne": "Okrem operačného systému vyberte aspoň jeden cieľ aktualizácie.",
"cancel": "Zrušiť",
"save": "Uložiť",
"delete": "Odstrániť konfiguráciu",
"deleteConfirm": "Odstrániť túto konfiguráciu hromadnej aktualizácie?",
"saveFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo uložiť.",
"deleteFailed": "Konfiguráciu hromadnej aktualizácie sa nepodarilo odstrániť.",
"planFailed": "Hromadnú aktualizáciu sa nepodarilo pripraviť. Skontrolujte nedostupné metódy."
},
"appEditor": {
"closePanel": "Zavrieť panel",
"cancelButton": "Zrušiť",
@@ -1283,6 +1342,17 @@
"hiddenBadge": "Momentálne skryté — po obnovení sa znova zobrazí v zozname nájdených aplikácií",
"registerDifferent": "Pridať inú aplikáciu",
"detectedInContainer": "Nájdené v tomto kontajneri",
"versionDetected": "Verzia {version} zistená v tomto kontajneri",
"legacyDetectorUsed": "Zistené pomocou staršieho kompatibilného detektora",
"dockerImagesTitle": "Aktualizácie Docker obrazov",
"dockerImagesHelp": "Kontroluje každý lokálny obraz oddelene od Docker enginu porovnaním nemenných digestov registra.",
"refreshDockerImages": "Skontrolovať teraz",
"noDockerImages": "Nenašli sa žiadne označené Docker obrazy",
"usedByContainers": "Používa: {names}",
"imageUpdateAvailable": "Dostupný nový obraz",
"imageUpToDate": "Aktuálne",
"imageDigestUnknown": "Digest nie je dostupný",
"dockerReadOnlyNote": "Kontrola iba na čítanie: žiadny obraz sa nesťahuje a žiadny kontajner sa nereštartuje.",
"installedManaged": "Nainštalované a spravované cez ProxMenux",
"nameLabel": "Názov",
"installedViaLabel": "Spôsob inštalácie",
@@ -1339,6 +1409,17 @@
"jsonPathPlaceholder": "napr. data.version alebo releases[0].tag_name",
"dockerImageLabel": "Obraz na Docker Hub",
"dockerImagePlaceholder": "napr. linuxserver/plex alebo nginx",
"dockerVersionedTagsHelp": "Použite iba pre verziované tagy. Pre latest, stable alebo lts použite aktualizácie Docker obrazov podľa digestu.",
"dockerPresetSemver": "Iba SemVer",
"dockerPresetSemverSuffix": "SemVer + prípona",
"dockerTagFilterHelp": "Filtruje skutočné tagy pred výberom najvyššej verzie.",
"dockerTagPreviewLabel": "Náhľad skutočných tagov",
"dockerTagPreviewCount": "{matched} zhodných z {scanned}",
"dockerTagPreviewLoading": "Kontroluje sa Docker Hub…",
"dockerTagPreviewFailed": "Tagy Docker Hub sa nepodarilo zobraziť",
"dockerTagPreviewEmpty": "Tomuto filtru nezodpovedá žiadny skutočný tag.",
"dockerMovingTag": "pohyblivý tag",
"dockerMovingTagHelp": "Pohyblivé tagy neobsahujú verziu. Sledujte ich podľa digestu v aktualizáciách Docker obrazov.",
"tagRegexLabel": "Regex tagu (so zachytávacou skupinou)",
"tagRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1371,7 +1452,14 @@
"registerCustom": "Pridať vlastnú aplikáciu",
"noAppsTitle": "Nie sú pridané žiadne aplikácie",
"noAppsBody": "Pridajte aplikácie, ktoré bežia v tomto kontajneri. Získate odkazy na ich webové rozhranie a voliteľne aj sledovanie dostupných verzií s upozorneniami na nové vydania.",
"registerApplication": "Pridať aplikáciu",
"registerApplication": "Registrovať aplikáciu",
"searchApplications": "Hľadať aplikácie",
"searchingApplications": "Hľadajú sa aplikácie…",
"noApplicationsDetected": "Neboli zistené žiadne aplikácie.",
"noNewApplicationsDetected": "Neboli zistené žiadne nové aplikácie.",
"oneNewApplicationDetected": "Bola zistená jedna nová aplikácia.",
"newApplicationsDetected": "Boli zistené {count} nové aplikácie.",
"detectionFailed": "Zisťovanie aplikácií zlyhalo",
"hiddenSuffix": "skryté: {count}",
"confirmHide": "Natrvalo skryť nájdenú aplikáciu „{name}“ v tomto kontajneri?\n\nAk ju budete chcieť neskôr vrátiť, kliknite na „Pridať inú aplikáciu“. V zozname skrytých aplikácií ju potom môžete obnoviť.",
"saveFailed": "Uloženie zlyhalo",
@@ -1382,6 +1470,13 @@
"binaryArgsHintGrafana": "Grafana potrebuje",
"loadFailed": "Nastavenia aplikácií sa nepodarilo načítať",
"checkFailed": "Kontrola zlyhala",
"testDetectorButton": "Otestovať detektor",
"testingDetectorButton": "Testovanie detektora…",
"detectorTestTitle": "Výsledok testu detektora",
"detectorTestNotSaved": "Iba koncept · neuložené",
"detectorTestNoVersion": "Nebola zistená žiadna verzia",
"detectorTestNoUpstream": "Nie je nastavený zdroj upstream",
"detectorTestFailed": "Test detektora zlyhal",
"deleteFailed": "Odstránenie zlyhalo",
"dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť",
"restoreFailed": "Nájdenú aplikáciu sa nepodarilo obnoviť",
@@ -1398,6 +1493,10 @@
"webLinks": "Webové odkazy",
"addPort": "Pridať port",
"detectedPorts": "Porty nájdené v kontajneri — kliknutím ich pridáte:",
"dockerPublishedServicesTitle": "Služby Docker so zverejnenými portmi",
"dockerPublishedServicesHelp": "Vyberte iba porty, ktoré poskytujú webové rozhranie. ProxMenux tieto kontajnery nezaregistruje ako samostatné aplikácie LXC.",
"dockerPublishedAdd": "Pridať odkaz",
"dockerPublishedPort": "port kontajnera {containerPort} → {hostPort} v LXC",
"noWebPorts": "Nenašli sa žiadne aktívne webové porty. Odkaz môžete vytvoriť ručne cez Pridať port.",
"trackUpstream": "Sledovať dostupnú verziu (voliteľné)",
"trackOff": "Vypnuté — iba odkaz",
@@ -1415,7 +1514,7 @@
"checkButton": "Skontrolovať",
"editFieldsButton": "Upraviť údaje",
"alsoDetectedContainer": "Ďalšie aplikácie nájdené v kontajneri",
"addAnotherApplication": "Pridať ďalšiu aplikáciu",
"addAnotherApplication": "Registrovať ďalšiu aplikáciu",
"doneButton": "Hotovo",
"editButton": "Upraviť",
"upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "OS + applikation",
"lastRun": "Senaste körningen: {date}",
"runSuccess": "✓ framgång",
"runPartial": "delvis slutförd",
"runFailed": "✗ misslyckades",
"runDeferred": "uppskjuten av säkerhetsskäl",
"runSkipped": "inget väntar",
"releaseDelaySummary": "{days} dagars väntetid",
"releaseDelayLabel": "Vänta efter en ny appversion",
"releaseDelayNone": "Ingen väntetid",
"releaseDelayDays": "Vänta {days} dag(ar)",
"releaseDelayHelp": "Gäller endast valda appar med versionsspårning. Deras väntande versioner måste vara så här gamla; appar utan versionsspårning kör sin uppdaterare vid varje schemalagt tillfälle.",
"notScheduled": "Inte schemalagt",
"frequency": "Frekvens",
"cronExpression": "Cron uttryck",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Endast OS-paket",
"targetOptionApp": "Endast ansökan",
"targetOptionBoth": "OS + applikation",
"dockerComposeTarget": "Docker Compose · {project}",
"dockerContainerTarget": "Docker-container · {container}",
"dockerEngineTarget": "Docker Engine",
"noTargets": "Inga mål valda",
"selectAtLeastOne": "Välj minst ett uppdateringsmål.",
"deleteButton": "Ta bort schema",
"deleteConfirm": "Vill du ta bort de schemalagda uppdateringarna för den här behållaren? Tillämpa standardvärden (säkerhetskopiering + omstart) behålls."
},
@@ -1217,7 +1230,7 @@
"osPlusApp": "OS + {appName} uppdateringar",
"osPlusApps": "OS + Apps-uppdateringar",
"noUpdateMethodTitle": "Ingen uppdateringsmetod tillgänglig",
"noUpdateMethodBody": "Ingen uppdateringsmetod tillgänglig. Uppdatera från själva appen eller ställ in ett anpassat kommando på fliken App.",
"noUpdateMethodBody": "Ingen uppdateringsmetod har identifierats för den här appen. Lägg till ett anpassat uppdateringskommando.",
"ociImmutableTitle": "OCI-bildbehållare",
"ociImmutableBody": "OS-paket bakas in vid bildbyggetid och kan inte uppdateras på plats. Tillämpa uppdateringar manuellt eller installera om med en nyare bild.",
"hideNoticeButton": "Dölj det här meddelandet för den här appen",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Tillämpningen",
"installedLabel": "installerat",
"upToDateAtLabel": "Uppdaterad kl",
"versionLabel": "version {version}",
"versionTrackingPending": "Versionsspårning väntar – nästa schemalagda kontroll visar installerad och tillgänglig version.",
"customCommandTitle": "Anpassat uppdateringskommando",
"customCommandBody": "Kör ett användardefinierat skalkommando för att uppdatera den här appen inuti behållaren.",
@@ -1238,6 +1252,7 @@
"hideNoticeAction": "Dölj detta meddelande för {appName}",
"detectedByPrefix": "Upptäckt av",
"editApp": "Redigera",
"configureUpdater": "Konfigurera",
"alsoDetectedTitle": "Detekteras även i den här behållaren:",
"detectedInline": "· upptäckt av {method}",
"customCommandLabel": "Anpassat uppdateringskommando",
@@ -1245,9 +1260,9 @@
"removeButton": "Ta bort",
"cancelButton": "Avbryt",
"saveButton": "Spara",
"editCommandButton": "Redigera kommando",
"wireUpCommandButton": "Lägg till anpassat uppdateringskommando",
"versionTrackingPendingShort": "Väntande versionsspårning.",
"versionTrackingNotConfigured": "Versionsspårning är inte konfigurerad.",
"managedByOsPackages": "Den här appen uppdateras via åtgärden för operativsystemspaket.",
"noMethodBody": "Ingen uppdateringsmetod tillgänglig. Uppdatera från själva appen, eller koppla upp ett anpassat kommando som ProxMenux kommer att köra inuti behållaren.",
"noMethodHideBody": "Denna app har ingen uppdateringsmetod registrerad. Ställ in en från fliken App eller avvisa det här meddelandet.",
"hideForApp": "Dölj meddelande för {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Ingen uppdateringsinformation ännu — kolla från Säkerhet → Secure Gateway.",
"ociTitle": "OCI-bildbehållare",
"ociBody": "Den här behållaren skapades från en OCI-bild (Docker). Uppdateringshantering för OCI-behållare kommer med den kommande OCI-installationsfunktionen - uppdateringar kommer att bygga om behållaren från en nyare bildtagg snarare än att patcha paket inuti.",
"helperNotUpdateable": "Community-scripts-registret markerar denna app som inte uppdateringsbar.",
"helperDetectedTitle": "Upptäckte en hjälparskriptuppdatering",
"helperDetectedBody": "Att applicera manuellt är säkrast.",
"dockerImagesTitle": "Docker-avbilder",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Avbilder",
"dockerImagesReadOnly": "Taggar jämförs med registrets digest. Compose-tjänster kan hämta avbilden och återskapas från sitt deklarerade projekt.",
"dockerEngineManagedByOs": "Docker Engine {version} hanteras som paket och uppdateras av OS-paketåtgärden ovan.",
"dockerEngineSubheading": "Motor",
"dockerEngineDetected": "Docker Engine {version} identifierad",
"dockerEngineUpdateHelp": "Uppdaterar endast installerade Docker-paket och nödvändiga beroenden. Andra operativsystemspaket och behållare ändras inte.",
"updateDockerEngineOnly": "Uppdatera Docker Engine",
"adguardWebUpdateOnly": "Den här installationen av AdGuard Home uppdateras i dess eget webbgränssnitt.",
"openAdguard": "Öppna AdGuard Home",
"updateDockerImage": "Uppdatera avbild",
"recreateStandaloneContainer": "Uppdatera {container}",
"standaloneRecreatePending": "Fristående containrar kräver skyddad återskapning: {names}",
"noDockerImages": "Docker är installerat men inga taggade avbilder hittades.",
"dockerInventoryStarting": "Väntar på att Docker ska starta klart…",
"dockerInventoryUnavailable": "Docker-inventeringen kunde inte läsas. Kontrollera igen när Docker är tillgängligt.",
"usedByContainers": "Används av: {names}",
"imageUpdateAvailable": "Ny avbild tillgänglig",
"imageUpToDate": "Uppdaterad",
"imageInstalledTag": "installerad tagg",
"imageDigestUnknown": "Digest saknas",
"dockerPendingSummary": "{count} uppdateringar av Docker-avbilder hittades. Använd tillhörande Docker- eller Compose-flöde.",
"postApplyChecking": "Verifierar uppdateringsresultat...",
"postApplyAllOk": "{count} paket(en) har tillämpats framgångsrikt — inget väntande.",
"postApplyNothingPending": "Inget väntande — allt är uppdaterat.",
"postApplyPartial": "{pending} paket som fortfarande väntar efter körningen.",
"postApplyPartialSubline": "{applied} tillämpas.Vissa uppdateringar slutfördes inte – granska terminalutgången ovan."
},
"bulkUpdate": {
"title": "Gruppuppdatering",
"description": "Konfigurera en åtgärd som uppdaterar operativsystemet och de valda metoderna i behållaren.",
"loading": "Läser in konfiguration…",
"notConfigured": "Inte konfigurerad.",
"configure": "Konfigurera",
"edit": "Redigera",
"apply": "Tillämpa uppdateringar",
"osTarget": "Operativsystem",
"osRequired": "Krävs för en gruppuppdatering.",
"includesDependencies": "Inkluderar även deklarerade beroenden: {names}",
"noMethod": "Ingen körbar uppdateringsmetod är tillgänglig.",
"staleTarget": "Det här valet är inte längre tillgängligt. Ta bort det innan du sparar.",
"dockerInventoryPending": "Väntar på inventeringen av Docker-avbilder…",
"missingDockerTarget": "Docker-avbilden är inte tillgänglig",
"selectAtLeastOne": "Välj minst ett uppdateringsmål utöver operativsystemet.",
"cancel": "Avbryt",
"save": "Spara",
"delete": "Ta bort konfiguration",
"deleteConfirm": "Ta bort den här gruppuppdateringskonfigurationen?",
"saveFailed": "Det gick inte att spara gruppuppdateringskonfigurationen.",
"deleteFailed": "Det gick inte att ta bort gruppuppdateringskonfigurationen.",
"planFailed": "Det gick inte att förbereda gruppuppdateringen. Kontrollera otillgängliga metoder."
},
"appEditor": {
"closePanel": "Stäng panelen",
"cancelButton": "Avbryt",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "För närvarande dold — kommer att dyka upp igen i upptäcktslistan",
"registerDifferent": "Registrera en annan app",
"detectedInContainer": "Upptäcks på den här behållaren",
"versionDetected": "Version {version} identifierad i den här containern",
"legacyDetectorUsed": "Upptäckt med en äldre kompatibilitetsreserv",
"dockerImagesTitle": "Uppdateringar av Docker-avbilder",
"dockerImagesHelp": "Kontrollerar varje lokal avbild separat från Docker-motorn genom att jämföra oföränderliga register-digests.",
"refreshDockerImages": "Kontrollera nu",
"noDockerImages": "Inga taggade Docker-avbilder hittades",
"usedByContainers": "Används av: {names}",
"imageUpdateAvailable": "Ny avbild tillgänglig",
"imageUpToDate": "Uppdaterad",
"imageDigestUnknown": "Digest saknas",
"dockerReadOnlyNote": "Skrivskyddad kontroll: ingen avbild hämtas och ingen container startas om.",
"installedManaged": "Installerad och hanterad av ProxMenux",
"nameLabel": "Namn",
"installedViaLabel": "Installerad via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "t.ex. data.version eller releases[0].tag_name",
"dockerImageLabel": "Docker Hub-bild",
"dockerImagePlaceholder": "t.ex. linuxserver/plex eller nginx",
"dockerVersionedTagsHelp": "Använd endast för versionerade taggar. För latest, stable eller lts använder du Docker-avbildningsuppdateringar med digest-jämförelse.",
"dockerPresetSemver": "Endast SemVer",
"dockerPresetSemverSuffix": "SemVer + suffix",
"dockerTagFilterHelp": "Filtrerar verkliga taggar innan ProxMenux väljer den högsta versionen.",
"dockerTagPreviewLabel": "Förhandsvisning av verkliga taggar",
"dockerTagPreviewCount": "{matched} matchade av {scanned}",
"dockerTagPreviewLoading": "Kontrollerar Docker Hub…",
"dockerTagPreviewFailed": "Kunde inte visa Docker Hub-taggar",
"dockerTagPreviewEmpty": "Inga verkliga taggar matchar filtret.",
"dockerMovingTag": "rörlig tagg",
"dockerMovingTagHelp": "Rörliga taggar innehåller ingen version. Spåra dem i stället via digest under Docker-avbildningsuppdateringar.",
"tagRegexLabel": "Taggregex (med fångstgrupp)",
"tagRegexPlaceholder": "t.ex. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registrera en anpassad app",
"noAppsTitle": "Inga ansökningar registrerade",
"noAppsBody": "Registrera apparna som körs i behållaren för att få webblänkar och, om du vill, spårning av tillgängliga versioner med aviseringar om nya utgåvor.",
"registerApplication": "Registrera ansökan",
"registerApplication": "Registrera app",
"searchApplications": "Sök appar",
"searchingApplications": "Söker efter appar…",
"noApplicationsDetected": "Inga appar identifierades.",
"noNewApplicationsDetected": "Inga nya appar identifierades.",
"oneNewApplicationDetected": "En ny app identifierades.",
"newApplicationsDetected": "{count} nya appar identifierades.",
"detectionFailed": "Appidentifieringen misslyckades",
"hiddenSuffix": "{count} dold",
"confirmHide": "Vill du dölja \"{name}\"-detekteringen permanent på den här behållaren?\n\nFör att ta tillbaka den senare, klicka på \"Registrera en annan app\" - den dolda listan visas där med ett återställningsalternativ.",
"saveFailed": "Det gick inte att spara",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana behöver",
"loadFailed": "Det gick inte att läsa in appkonfigurationen",
"checkFailed": "Kontrollen misslyckades",
"testDetectorButton": "Testa detektor",
"testingDetectorButton": "Testar detektorn…",
"detectorTestTitle": "Resultat av detektortest",
"detectorTestNotSaved": "Endast utkast · inte sparat",
"detectorTestNoVersion": "Ingen version identifierades",
"detectorTestNoUpstream": "Ingen uppströmskälla är konfigurerad",
"detectorTestFailed": "Detektortestet misslyckades",
"deleteFailed": "Det gick inte att ta bort",
"dismissFailed": "Det gick inte att avvisa upptäckten",
"restoreFailed": "Det gick inte att återställa upptäckten",
@@ -1399,6 +1494,10 @@
"webLinks": "Webblänkar",
"addPort": "Lägg till port",
"detectedPorts": "Portar upptäckta i behållaren — klicka för att lägga till:",
"dockerPublishedServicesTitle": "Docker-tjänster med publicerade portar",
"dockerPublishedServicesHelp": "Välj endast portar som tillhandahåller ett webbgränssnitt. ProxMenux registrerar inte dessa containrar som fristående LXC-appar.",
"dockerPublishedAdd": "Lägg till länk",
"dockerPublishedPort": "containerport {containerPort} → {hostPort} i LXC",
"noWebPorts": "Inga webbportar lyssnar. Använd Lägg till port om du fortfarande vill skapa en länk manuellt.",
"trackUpstream": "Spåra tillgänglig version (valfritt)",
"trackOff": "Av – endast länk",
@@ -1416,7 +1515,7 @@
"checkButton": "Kontrollera",
"editFieldsButton": "Redigera fält",
"alsoDetectedContainer": "Detekteras även på denna behållare",
"addAnotherApplication": "Lägg till ytterligare ett program",
"addAnotherApplication": "Registrera en annan app",
"doneButton": "Gjort",
"editButton": "Redigera",
"upstreamErrorTimeout": "Nätverkstimeout vid kontakt uppströms",
+4
View File
@@ -127,6 +127,10 @@ cp "$SCRIPT_DIR/disk_temperature_history.py" "$APP_DIR/usr/bin/" 2>/dev/null ||
cp "$SCRIPT_DIR/health_thresholds.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_thresholds.py not found"
cp "$SCRIPT_DIR/managed_installs.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ managed_installs.py not found"
cp "$SCRIPT_DIR/lxc_apps.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ lxc_apps.py not found"
cp "$SCRIPT_DIR/recreate_docker_container.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ recreate_docker_container.py not found"
chmod +x "$APP_DIR/usr/bin/recreate_docker_container.py" 2>/dev/null || true
cp "$SCRIPT_DIR/update_docker_engine.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ update_docker_engine.py not found"
chmod +x "$APP_DIR/usr/bin/update_docker_engine.py" 2>/dev/null || true
cp "$APPIMAGE_ROOT/../json/app_tracking_hints.json" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ app_tracking_hints.json not found"
cp "$SCRIPT_DIR/flask_terminal_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_terminal_routes.py not found"
cp "$SCRIPT_DIR/hardware_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ hardware_monitor.py not found"
File diff suppressed because it is too large Load Diff
+39
View File
@@ -108,6 +108,32 @@ sock = Sock()
# Active terminal sessions
active_sessions = {}
_script_completion_hook = None
_script_completion_hook_lock = threading.Lock()
def set_script_completion_hook(callback):
"""Register the backend hook invoked after a streamed script exits."""
global _script_completion_hook
with _script_completion_hook_lock:
_script_completion_hook = callback
def _run_script_completion_hook(script_path, params, exit_code, duration_seconds):
with _script_completion_hook_lock:
callback = _script_completion_hook
if callback is None:
return
try:
callback(
script_path=script_path,
params=dict(params or {}),
exit_code=int(exit_code),
duration_seconds=max(0, int(duration_seconds)),
)
except Exception as exc:
print(f"[ProxMenux] script completion hook failed: {exc}", flush=True)
@terminal_bp.route('/api/terminal/health', methods=['GET'])
def terminal_health():
"""Health check for terminal service"""
@@ -470,6 +496,7 @@ def script_websocket(ws, session_id):
env['PYTHONUNBUFFERED'] = '1'
env['TERM'] = 'xterm-256color'
script_started_at = time.monotonic()
script_process = subprocess.Popen(
['/bin/bash', script_path],
stdin=slave_fd,
@@ -579,6 +606,18 @@ def script_websocket(ws, session_id):
script_process.wait()
exit_code = script_process.returncode if script_process.returncode is not None else 0
threading.Thread(
target=_run_script_completion_hook,
args=(
script_path,
params,
exit_code,
time.monotonic() - script_started_at,
),
daemon=True,
name=f'script-complete-{session_id}',
).start()
try:
ws.send(f'\r\n[Script exited with code {exit_code}]\r\n')
File diff suppressed because it is too large Load Diff
+242 -107
View File
@@ -167,6 +167,26 @@ def _detect_nvidia_xfree86() -> Optional[dict]:
# libedgetpu1-std from Google's apt repo).
def _coral_pcie_hardware_present() -> bool:
"""True when a Coral PCIe/M.2 device (vendor 0x1ac1, Global Unichip
Corp.) is visible on the PCI bus. Used together with the gasket-dkms
package state to detect orphan installs left behind by the legacy
installer (`scripts/install_coral_pve.sh` before 2026-04) that
installed the DKMS driver unconditionally on USB-only hosts."""
try:
for entry in os.listdir("/sys/bus/pci/devices"):
try:
with open(f"/sys/bus/pci/devices/{entry}/vendor",
"r", encoding="utf-8") as fh:
if fh.read().strip() == "0x1ac1":
return True
except OSError:
continue
except OSError:
pass
return False
def _detect_coral_host() -> list[dict]:
out: list[dict] = []
@@ -180,61 +200,105 @@ def _detect_coral_host() -> list[dict]:
# knows the fork's patch level.
# 2. `dpkg-query gasket-dkms` — the Debian package version, only
# present when the user installed via .deb rather than the
# ProxMenux script.
# ProxMenux script. Package state matters: only `ok installed`
# is trusted as a real version; broken states surface as
# "package present but not usable" so the UI can offer cleanup
# instead of a spurious "update available".
# 3. `dkms status` — the upstream module version registered with
# DKMS, which is always the bare `1.0`. Useful as a "modules
# are present" indicator but doesn't reveal the fork patch
# level, so the update-availability check would always fire a
# false positive against feranick's `1.0-N` tags. Reported on
# .50 after a successful re-install kept showing the update
# notification.
pcie_version: Optional[str] = None
# false positive against feranick's `1.0-N` tags.
#
# Orphan detection: gasket-dkms package present + no PCIe/M.2
# hardware = residue from the legacy installer. `_gasket_orphan`
# is exposed so `install_coral.sh` and the notification pipeline
# can offer cleanup without ever calling it "an update".
pcie_hw_present = _coral_pcie_hardware_present()
marker_version: Optional[str] = None
try:
with open("/var/lib/proxmenux/coral_gasket_version",
"r", encoding="utf-8", errors="replace") as fh:
marker = fh.read().strip()
# Sanity check: the file should hold something that looks
# like a version tag, not an error message or empty line.
if marker and re.match(r"^[A-Za-z0-9._+-]+$", marker):
pcie_version = marker
marker_version = marker
except OSError:
pass
if not pcie_version:
try:
r = subprocess.run(
["dpkg-query", "-W", "-f=${Status}|${Version}", "gasket-dkms"],
capture_output=True, text=True, timeout=3,
)
if r.returncode == 0 and "ok installed" in r.stdout:
pcie_version = r.stdout.split("|", 1)[1].strip()
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass
if not pcie_version:
try:
r = subprocess.run(
["dkms", "status"], capture_output=True, text=True, timeout=3,
)
if r.returncode == 0:
for line in r.stdout.splitlines():
if line.startswith("gasket"):
# "gasket, 1.0, ..." or "gasket/1.0, ..."
m = re.match(r"^gasket[, /]([^,\s]+)", line)
if m:
pcie_version = m.group(1)
break
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass
if pcie_version:
out.append({
# gasket-dkms package inspection: state + version, kept separate.
dpkg_state: str = "absent" # "healthy" | "broken" | "absent"
dpkg_version: Optional[str] = None
try:
r = subprocess.run(
["dpkg-query", "-W", "-f=${Status}|${Version}", "gasket-dkms"],
capture_output=True, text=True, timeout=3,
)
if r.returncode == 0 and "|" in r.stdout:
status_part, _, version_part = r.stdout.partition("|")
if "ok installed" in status_part:
dpkg_state = "healthy"
dpkg_version = version_part.strip() or None
elif any(tok in status_part for tok in (
"half-configured", "half-installed", "unpacked",
"failed-config", "reinst-required", "trigger",
)):
dpkg_state = "broken"
dpkg_version = version_part.strip() or None
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass
# Version resolution: emit only when we can actually trust it, i.e.
# the hardware is present AND either we have a marker file or the
# package is healthy. On broken or orphan states we intentionally
# omit `current_version` so the update comparator never fires a
# false "update available" against feranick's tags.
pcie_version: Optional[str] = None
if pcie_hw_present:
if marker_version:
pcie_version = marker_version
elif dpkg_state == "healthy" and dpkg_version:
pcie_version = dpkg_version
else:
# Fallback to dkms status ONLY when hardware is present and
# no better source exists. Kept for backwards compatibility
# with hosts that lost the marker file after a manual dkms
# rebuild but still have working hardware + working modules.
try:
r = subprocess.run(
["dkms", "status"], capture_output=True, text=True, timeout=3,
)
if r.returncode == 0:
for line in r.stdout.splitlines():
if line.startswith("gasket"):
m = re.match(r"^gasket[, /]([^,\s]+)", line)
if m:
pcie_version = m.group(1)
break
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass
is_orphan = (dpkg_state != "absent") and not pcie_hw_present
# Emit the entry whenever we have a trustworthy version OR whenever
# there is package state to surface (broken / orphan). This lets the
# frontend and the notification pipeline see both healthy installs
# and the two remediation cases in the same registry shape.
if pcie_version or dpkg_state != "absent":
entry = {
"id": "coral-host-pcie",
"type": "coral_host",
"name": "Coral TPU Driver (gasket-dkms)",
"current_version": pcie_version,
"menu_label": "GPU & TPU → Coral TPU",
"menu_script": "scripts/gpu_tpu/install_coral.sh",
"_coral_variant": "pcie",
})
"_gasket_pkg_state": dpkg_state,
"_gasket_orphan": is_orphan,
"_gasket_pcie_hardware_present": pcie_hw_present,
}
if pcie_version:
entry["current_version"] = pcie_version
out.append(entry)
# USB — libedgetpu1-std (default) or libedgetpu1-max if the user
# opted into the overclocked runtime. Either one means the USB
@@ -551,7 +615,11 @@ _helpers_cache_lock = threading.RLock()
_helpers_cache: Optional[dict] = None
_helpers_cache_ts: float = 0.0
_UPDATE_SLUG_RE = re.compile(r"ct/([a-z0-9_-]+)\.sh")
_UPDATE_SLUG_RE = re.compile(r"ct/([a-z0-9._-]+)\.sh")
_BASE_OS_HELPER_SLUGS = frozenset({
"alpine", "archlinux", "archlinux-vm", "debian", "fedora",
"gentoo", "opensuse", "ubuntu",
})
def _fetch_helpers_cache() -> dict:
@@ -714,22 +782,30 @@ def _guess_helper_slug_from_hostname(hostname: str) -> Optional[str]:
return None
def _infer_helper_slug(vmid: str, hostname: str) -> Optional[str]:
"""Best-effort identification of the community-scripts slug for a CT.
def _identify_helper_slug(vmid: str, hostname: str) -> tuple[Optional[str], Optional[str]]:
"""Return ``(slug, evidence_source)`` for a community-scripts CT.
Primary: extract from /usr/bin/update (present on installs from a
reasonably modern community-scripts installer). Fallback: if the
CT carries a helper-scripts tag but /usr/bin/update is missing
(very old installs, or the file was removed), guess by
fuzzy-matching the hostname against the helpers_cache slug list.
``update_wrapper`` is executable evidence: the slug was extracted
from /usr/bin/update. ``tag_hostname`` is only an identity hint for
old installs and must never enable an update action by itself.
"""
slug = _probe_helper_scripts_slug(vmid)
if slug:
return slug
return slug, "update_wrapper"
tags = _probe_lxc_tags(vmid)
if not (tags & _HELPER_SCRIPTS_TAGS):
return None
return _guess_helper_slug_from_hostname(hostname)
return None, None
slug = _guess_helper_slug_from_hostname(hostname)
return (slug, "tag_hostname") if slug else (None, None)
def _infer_helper_slug(vmid: str, hostname: str) -> Optional[str]:
"""Backward-compatible identity-only wrapper.
Callers deciding whether an updater may run must use
:func:`_identify_helper_slug` and require ``update_wrapper``.
"""
return _identify_helper_slug(vmid, hostname)[0]
def _probe_lxc_os(vmid: str) -> Optional[str]:
@@ -766,7 +842,7 @@ def _probe_lxc_os(vmid: str) -> Optional[str]:
return None
def _detect_lxc_containers() -> list[dict]:
def _detect_lxc_containers(only_vmid: Optional[int] = None) -> list[dict]:
"""Enumerate running Debian/Ubuntu CTs as registry entries.
OS detection is cached in the registry entry (`_os_family`), so the
@@ -809,6 +885,8 @@ def _detect_lxc_containers() -> list[dict]:
out: list[dict] = []
for ct in cts:
if only_vmid is not None and str(ct.get("vmid")) != str(int(only_vmid)):
continue
if ct["status"] != "running":
continue
vmid = ct["vmid"]
@@ -853,16 +931,23 @@ def _detect_lxc_containers() -> list[dict]:
# Jellyfin" rather than a generic "Update").
has_app_updater = False
helper_slug: Optional[str] = None
helper_slug_source: Optional[str] = None
helper_app_name: Optional[str] = None
helper_updateable_known = False # True when we found the slug in the cache
if not is_oci and not managed_oci_app:
helper_slug = _infer_helper_slug(vmid, ct.get("name") or "")
helper_slug, helper_slug_source = _identify_helper_slug(
vmid, ct.get("name") or ""
)
if helper_slug:
entry = _fetch_helpers_cache().get(helper_slug)
if entry:
helper_updateable_known = True
helper_app_name = entry.get("name") or helper_slug
has_app_updater = bool(entry.get("updateable"))
has_app_updater = bool(
helper_slug_source == "update_wrapper"
and helper_slug not in _BASE_OS_HELPER_SLUGS
and entry.get("updateable")
)
out.append({
"id": cid,
@@ -877,6 +962,7 @@ def _detect_lxc_containers() -> list[dict]:
"_managed_oci_app": managed_oci_app,
"_has_app_updater": has_app_updater,
"_helper_slug": helper_slug,
"_helper_slug_source": helper_slug_source,
"_helper_app_name": helper_app_name,
"_helper_updateable_known": helper_updateable_known,
})
@@ -904,6 +990,45 @@ def _normalise_detector_result(result: Any) -> list[dict]:
return []
def _merge_detected_entry(existing: dict, entry: dict, now: str) -> dict:
"""Refresh one registry row from detector evidence without touching peers."""
if existing.get("removed_at"):
existing.pop("removed_at", None)
existing["reactivated_at"] = now
for key in ("name", "current_version", "menu_label", "menu_script"):
if key in entry and entry[key] is not None:
existing[key] = entry[key]
for key, value in entry.items():
if key.startswith("_"):
existing[key] = value
existing["last_seen"] = now
return existing
def _new_detected_entry(entry: dict, now: str) -> dict:
new_entry = {
"id": entry["id"],
"type": entry.get("type", "unknown"),
"name": entry.get("name", entry["id"]),
"current_version": entry.get("current_version"),
"menu_label": entry.get("menu_label"),
"menu_script": entry.get("menu_script"),
"installed_by": "detected",
"first_seen": now,
"last_seen": now,
"update_check": {
"last_check": None,
"available": False,
"latest": None,
"error": None,
},
}
for key, value in entry.items():
if key.startswith("_"):
new_entry[key] = value
return new_entry
def detect_and_register() -> dict:
"""Run every detector, merge results into the registry, persist.
@@ -938,44 +1063,9 @@ def detect_and_register() -> dict:
# 1. Add new + reactivate / refresh existing.
for item_id, entry in discovered.items():
if item_id in index:
existing = items[index[item_id]]
# Reactivate if it was previously removed
if existing.get("removed_at"):
existing.pop("removed_at", None)
existing["reactivated_at"] = now
# Refresh metadata fields that may have evolved
for k in ("name", "current_version", "menu_label", "menu_script"):
if k in entry and entry[k] is not None:
existing[k] = entry[k]
# Preserve internal helpers like `_oci_app_id`
for k, v in entry.items():
if k.startswith("_"):
existing[k] = v
existing["last_seen"] = now
_merge_detected_entry(items[index[item_id]], entry, now)
else:
# Brand new entry
new_entry = {
"id": entry["id"],
"type": entry.get("type", "unknown"),
"name": entry.get("name", entry["id"]),
"current_version": entry.get("current_version"),
"menu_label": entry.get("menu_label"),
"menu_script": entry.get("menu_script"),
"installed_by": "detected",
"first_seen": now,
"last_seen": now,
"update_check": {
"last_check": None,
"available": False,
"latest": None,
"error": None,
},
}
# Carry over internals (`_oci_app_id` etc.)
for k, v in entry.items():
if k.startswith("_"):
new_entry[k] = v
items.append(new_entry)
items.append(_new_detected_entry(entry, now))
# 2. Mark missing items as removed (don't delete — preserve
# history so a reinstall doesn't lose the audit trail).
@@ -1588,6 +1678,23 @@ _CHECKERS: dict[str, Callable[[dict], dict]] = {
}
def _store_update_result(item: dict, result: dict) -> None:
"""Apply one checker result using the registry's canonical shape."""
item["update_check"] = {
"available": bool(result.get("available")),
"latest": result.get("latest"),
"last_check": result.get("last_check") or _now_iso(),
"error": result.get("error"),
}
if result.get("current") and not item.get("current_version"):
item["current_version"] = result["current"]
for extra_key in ("_packages", "_upgrade_kind", "_kernel",
"_kernel_note", "_count", "_security_count",
"_coral_variant", "_coral_pkg"):
if extra_key in result:
item["update_check"][extra_key] = result[extra_key]
def check_for_updates(force: bool = False) -> list[dict]:
"""Run every type-specific checker over active items, persist
the updated state, return the list of items that have an update
@@ -1622,25 +1729,7 @@ def check_for_updates(force: bool = False) -> list[dict]:
result = {"available": False, "latest": None,
"last_check": _now_iso(), "error": str(e)}
it["update_check"] = {
"available": bool(result.get("available")),
"latest": result.get("latest"),
"last_check": result.get("last_check") or _now_iso(),
"error": result.get("error"),
}
if result.get("current") and not it.get("current_version"):
it["current_version"] = result["current"]
# Per-checker extras carried through into the persisted
# `update_check` blob. Add new keys here when a future
# checker needs to surface fields beyond available/latest.
# `_count` + `_security_count` were missing originally, so
# the LXC checker's counts dropped on the floor and the
# frontend badge couldn't render.
for extra_key in ("_packages", "_upgrade_kind", "_kernel",
"_kernel_note", "_count", "_security_count",
"_coral_variant", "_coral_pkg"):
if extra_key in result:
it["update_check"][extra_key] = result[extra_key]
_store_update_result(it, result)
if it["update_check"]["available"]:
updates_available.append(it)
@@ -1650,3 +1739,49 @@ def check_for_updates(force: bool = False) -> list[dict]:
_write_registry(reg)
return updates_available
def refresh_lxc(vmid: int) -> Optional[dict]:
"""Detect and refresh exactly one running LXC.
This is the lifecycle counterpart of the daily collector. It is called
after a stopped container starts and deliberately leaves every other
guest's registry row untouched.
"""
try:
target_vmid = int(vmid)
except (TypeError, ValueError):
return None
detected = _detect_lxc_containers(only_vmid=target_vmid)
if not detected:
return None
entry = detected[0]
item_id = entry["id"]
now = _now_iso()
with _lock:
reg = _read_registry()
items: list[dict] = list(reg.get("items", []))
target = next((item for item in items if item.get("id") == item_id), None)
if target is None:
target = _new_detected_entry(entry, now)
items.append(target)
else:
_merge_detected_entry(target, entry, now)
try:
result = _check_lxc_updates(target)
except Exception as exc:
result = {
"available": False,
"latest": None,
"last_check": _now_iso(),
"error": str(exc),
}
_store_update_result(target, result)
reg["items"] = items
reg["version"] = _SCHEMA_VERSION
reg["last_targeted_refresh"] = now
_write_registry(reg)
return dict(target)
+45 -2
View File
@@ -22,7 +22,7 @@ import sqlite3
import subprocess
import threading
from queue import Queue
from typing import Optional, Dict, Any, Tuple
from typing import Optional, Dict, Any, Tuple, Callable
from pathlib import Path
@@ -1939,8 +1939,16 @@ class TaskWatcher:
'vzmigrate': ('migration_start', 'INFO'),
}
def __init__(self, event_queue: Queue):
def __init__(
self,
event_queue: Queue,
guest_lifecycle_callback: Optional[Callable[[str, str, str], None]] = None,
):
self._queue = event_queue
# Reuse the exact PVE task transition already responsible for
# VM/CT lifecycle notifications. Consumers such as the modal cache
# can subscribe without introducing a second status poller.
self._guest_lifecycle_callback = guest_lifecycle_callback
self._running = False
self._thread: Optional[threading.Thread] = None
# `_hostname` is exposed as a @property below so every read returns
@@ -2250,6 +2258,31 @@ class TaskWatcher:
# Determine entity type from task type
entity = 'ct' if task_type.startswith('vz') else 'vm'
# A completed PVE lifecycle task is the existing source of truth for
# start/stop/restart notifications. Publish the same transition to
# the optional cache listener before notification-only suppression
# (backup/startup aggregation, disabled channels, cooldowns) so cache
# correctness never depends on whether a message is delivered.
lifecycle_actions = {
'qmstart': ('qemu', 'start'),
'qmstop': ('qemu', 'stop'),
'qmshutdown': ('qemu', 'stop'),
'qmreboot': ('qemu', 'reboot'),
'qmreset': ('qemu', 'reboot'),
'vzstart': ('lxc', 'start'),
'vzstop': ('lxc', 'stop'),
'vzshutdown': ('lxc', 'stop'),
'vzreboot': ('lxc', 'reboot'),
}
lifecycle = lifecycle_actions.get(task_type)
if (lifecycle and self._guest_lifecycle_callback
and not is_error and (status == 'OK' or is_warning)):
try:
self._guest_lifecycle_callback(vmid, lifecycle[0], lifecycle[1])
except Exception as exc:
print(f'[TaskWatcher] guest lifecycle callback failed for '
f'{lifecycle[0]} {vmid}: {exc}', flush=True)
# Backup completion/failure and replication events are handled
# EXCLUSIVELY by the PVE webhook, which delivers richer data (full
@@ -3516,6 +3549,15 @@ class PollingCollector:
try:
import lxc_apps
lxc_apps.refresh_all_apps(force=False)
# Docker images have an independent lifecycle from both the OS
# packages and the Docker engine. Refresh their read-only
# registry digest inventory on the same daily cadence; this never
# pulls or recreates containers.
# This is the single automatic Docker registry comparison. Force
# the rolling pass itself so a user-triggered check shortly after
# yesterday's cycle cannot postpone the next automatic scan by an
# additional day. Normal UI reads remain cache-only for 24 hours.
lxc_apps.refresh_docker_inventories(force=True)
# After the refresh, emit `app_update_available` for every
# sidecar entry currently flagged with a pending upstream
# release. `check_app(force=False)` short-circuits on a
@@ -3526,6 +3568,7 @@ class PollingCollector:
# moment. `notification_manager` dedups by entity_id
# (vmid + app_id + latest_version) so repeated calls only
# deliver one notification per release.
lxc_apps.emit_all_pending_docker_stacks()
lxc_apps.emit_all_pending_updates()
except Exception as e:
print(f"[PollingCollector] lxc_apps refresh failed: {e}")
+19 -1
View File
@@ -520,6 +520,8 @@ _AGGREGATION_EXEMPT_EVENTS = frozenset({
# at once, so without this exemption only the first 1-2 land and
# the rest get buffered into a useless summary.
'app_update_available',
'docker_stack_update_available',
'lxc_update_applied',
})
@@ -791,6 +793,7 @@ class NotificationManager:
self._task_watcher: Optional[TaskWatcher] = None
self._polling_collector: Optional[PollingCollector] = None
self._dispatch_thread: Optional[threading.Thread] = None
self._guest_lifecycle_callback = None
# Webhook receiver (no thread, passive)
self._hook_watcher: Optional[ProxmoxHookWatcher] = None
@@ -981,6 +984,17 @@ class NotificationManager:
with self._lock:
self._load_config()
return {'success': True, 'channels': list(self._channels.keys())}
def set_guest_lifecycle_callback(self, callback) -> None:
"""Attach a consumer to the existing PVE task lifecycle watcher.
Detection stays in TaskWatcher—the same source that emits VM/CT
start/stop notifications. This setter only lets Flask invalidate and
rebuild its guest caches when that already-detected event completes.
"""
self._guest_lifecycle_callback = callback
if self._task_watcher is not None:
self._task_watcher._guest_lifecycle_callback = callback
# ─── Server Mode (Background) ──────────────────────────────
@@ -1017,7 +1031,10 @@ class NotificationManager:
# polling collector keep the managed_installs registry, the
# error history, and the task state up to date.
self._journal_watcher = JournalWatcher(self._event_queue)
self._task_watcher = TaskWatcher(self._event_queue)
self._task_watcher = TaskWatcher(
self._event_queue,
guest_lifecycle_callback=self._guest_lifecycle_callback,
)
self._polling_collector = PollingCollector(self._event_queue)
self._journal_watcher.start()
@@ -1966,6 +1983,7 @@ class NotificationManager:
'coral_driver_update_available',
'secure_gateway_update_available',
'app_update_available',
'docker_stack_update_available',
# Security events that must not be silenced by stale cooldowns
# following a Monitor reinstall (Pedro Rico, 19/05).
'auth_fail',
+12 -4
View File
@@ -512,10 +512,7 @@ TEMPLATES = {
},
'lxc_update_applied': {
'title': '{hostname}: LXC {ct_name} ({vmid}) update {result}',
'body': (
'Container {ct_name} (CT {vmid}) — update {result}.\n'
'Target: {target} Duration: {duration}'
),
'body': '{details}',
'label': 'LXC update applied',
'group': 'vm_ct',
'default_enabled': True,
@@ -540,6 +537,16 @@ TEMPLATES = {
# never received the notification they explicitly asked for.
'default_enabled': True,
},
'docker_stack_update_available': {
'title': '{hostname}: Docker updates available on CT {vmid}',
'body': (
'Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n'
'{details}'
),
'label': 'Docker updates available',
'group': 'updates',
'default_enabled': True,
},
'vm_start': {
'title': '{hostname}: VM {vmname} ({vmid}) started',
'body': 'Virtual machine {vmname} (ID: {vmid}) is now running.',
@@ -1728,6 +1735,7 @@ EVENT_EMOJI = {
'lxc_updates_available': '\U0001F4E6', # \uD83D\uDCE6 package \u2014 pending CT updates
'lxc_update_applied': '\u2705', # \u2705 check \u2014 update applied
'app_update_available': '\U0001F195', # \ud83c\udd95 NEW \u2014 upstream app release
'docker_stack_update_available': '\U0001F433',
'vm_start': '\u25B6\uFE0F', # play button
'vm_start_warning': '\u26A0\uFE0F', # warning sign - started with warnings
'vm_stop': '\u23F9\uFE0F', # stop button
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Safely recreate one standalone Docker container inside an LXC.
The container's create-time Config/HostConfig is read from Docker's API,
the referenced image is pulled, and a replacement is validated before the
old container is removed. If create/start/validation fails, the original
container name and running state are restored.
Compose-owned containers are deliberately rejected: their declarative
project is the authoritative and safer update path.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import time
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
def pct_exec(vmid: int, argv: list[str], *, input_text: str | None = None, timeout: int = 300) -> subprocess.CompletedProcess:
return subprocess.run(
["/usr/sbin/pct", "exec", str(vmid), "--", *argv],
input=input_text,
capture_output=True,
text=True,
timeout=timeout,
)
def checked(vmid: int, argv: list[str], *, input_text: str | None = None, timeout: int = 300) -> str:
result = pct_exec(vmid, argv, input_text=input_text, timeout=timeout)
if result.returncode != 0:
detail = (result.stderr or result.stdout or "command failed").strip()
raise RuntimeError(f"{' '.join(argv[:3])}: {detail}")
return result.stdout or ""
def inspect_one(vmid: int, name: str) -> dict:
payload = json.loads(checked(vmid, ["docker", "inspect", name], timeout=30))
if not isinstance(payload, list) or len(payload) != 1:
raise RuntimeError("docker inspect returned an unexpected response")
return payload[0]
def create_payload(inspect: dict, image: str) -> dict:
config = dict(inspect.get("Config") or {})
config["Image"] = image
host_config = dict(inspect.get("HostConfig") or {})
if host_config.get("AutoRemove"):
raise RuntimeError("containers with AutoRemove cannot be recreated safely")
endpoints: dict[str, dict] = {}
for network_name, endpoint in ((inspect.get("NetworkSettings") or {}).get("Networks") or {}).items():
if not NAME_RE.match(str(network_name)):
continue
# Preserve names/aliases and driver options, but deliberately let
# Docker allocate a fresh IP while the stopped rollback container
# still owns its old endpoint.
target: dict = {}
for key in ("Aliases", "Links", "DriverOpts"):
if endpoint.get(key) is not None:
target[key] = endpoint[key]
endpoints[str(network_name)] = target
return {
**config,
"HostConfig": host_config,
"NetworkingConfig": {"EndpointsConfig": endpoints},
}
def api_create(vmid: int, name: str, payload: dict) -> str:
body = json.dumps(payload, separators=(",", ":"))
result = pct_exec(
vmid,
[
"curl", "--silent", "--show-error", "--fail-with-body",
"--unix-socket", "/var/run/docker.sock",
"-H", "Content-Type: application/json",
"-X", "POST", "--data-binary", "@-",
f"http://localhost/v1.41/containers/create?name={name}",
],
input_text=body,
timeout=60,
)
if result.returncode != 0:
raise RuntimeError((result.stderr or result.stdout or "Docker create API failed").strip())
response = json.loads(result.stdout or "{}")
container_id = str(response.get("Id") or "")
if not container_id:
raise RuntimeError(str(response.get("message") or "Docker create API returned no container id"))
return container_id
def recreate(vmid: int, name: str) -> None:
original = inspect_one(vmid, name)
labels = ((original.get("Config") or {}).get("Labels") or {})
if labels.get("com.docker.compose.project"):
raise RuntimeError("container belongs to Docker Compose; use its project update action")
image = str((original.get("Config") or {}).get("Image") or "").strip()
if not image:
raise RuntimeError("container has no reusable image reference")
was_running = bool((original.get("State") or {}).get("Running"))
backup_name = f"{name}.proxmenux-rollback-{int(time.time())}"
replacement_created = False
print(f"=== Docker protected recreation: CT {vmid} / {name} ===", flush=True)
print(f"Image: {image}", flush=True)
print("Pulling the referenced image…", flush=True)
pull = pct_exec(vmid, ["docker", "pull", image], timeout=1800)
if pull.stdout:
print(pull.stdout.rstrip(), flush=True)
if pull.returncode != 0:
raise RuntimeError((pull.stderr or "docker pull failed").strip())
payload = create_payload(original, image)
try:
if was_running:
print("Stopping the original container…", flush=True)
checked(vmid, ["docker", "stop", "--time", "30", name], timeout=60)
print(f"Keeping rollback container as {backup_name}…", flush=True)
checked(vmid, ["docker", "rename", name, backup_name], timeout=30)
print("Creating replacement from the inspected configuration…", flush=True)
api_create(vmid, name, payload)
replacement_created = True
if was_running:
checked(vmid, ["docker", "start", name], timeout=60)
deadline = time.time() + 20
while True:
state = inspect_one(vmid, name).get("State") or {}
if not state.get("Running"):
raise RuntimeError(str(state.get("Error") or "replacement stopped during validation"))
health = ((state.get("Health") or {}).get("Status") or "").lower()
if health == "unhealthy":
raise RuntimeError("replacement healthcheck is unhealthy")
if health != "starting" or time.time() >= deadline:
break
time.sleep(2)
print("Replacement validated; removing rollback container…", flush=True)
checked(vmid, ["docker", "rm", "-f", backup_name], timeout=60)
print("Docker container recreation completed successfully.", flush=True)
except Exception:
print("Recreation failed; restoring the original container…", file=sys.stderr, flush=True)
if replacement_created:
pct_exec(vmid, ["docker", "rm", "-f", name], timeout=60)
pct_exec(vmid, ["docker", "rename", backup_name, name], timeout=30)
if was_running:
pct_exec(vmid, ["docker", "start", name], timeout=60)
raise
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--vmid", required=True, type=int)
parser.add_argument("--container", required=True)
args = parser.parse_args()
if args.vmid <= 0 or not NAME_RE.match(args.container):
parser.error("invalid VMID or container name")
try:
recreate(args.vmid, args.container)
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr, flush=True)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""Update only the Docker Engine stack inside one Proxmox LXC.
This intentionally does not run the community-scripts Docker updater:
that updater also performs a full apt/apk upgrade. ProxMenux resolves a
small allow-list of Docker packages that are already installed and asks the
guest package manager to upgrade only those packages (and required
dependencies). Static/manual installations without a supported package
manager fail closed instead of guessing how they were installed.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
import time
APT_PACKAGES = (
"docker-ce",
"docker-ce-cli",
"docker-ce-rootless-extras",
"docker-buildx-plugin",
"docker-compose-plugin",
"docker-model-plugin",
"containerd.io",
"docker.io",
"docker-compose-v2",
"docker-compose",
"docker-buildx",
"docker-cli",
"containerd",
"runc",
"moby-engine",
"moby-cli",
"moby-buildx",
"moby-compose",
"moby-containerd",
)
APK_PACKAGES = (
"docker",
"docker-cli",
"docker-openrc",
"docker-cli-buildx",
"docker-cli-compose",
"docker-compose",
"containerd",
"runc",
)
RPM_PACKAGES = (
"docker-ce",
"docker-ce-cli",
"docker-ce-rootless-extras",
"docker-buildx-plugin",
"docker-compose-plugin",
"containerd.io",
"moby-engine",
"moby-cli",
"moby-buildx",
"moby-compose",
"moby-containerd",
)
def pct(vmid: int, argv: list[str], *, capture: bool = False) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["/usr/sbin/pct", "exec", str(vmid), "--", *argv],
text=True,
capture_output=capture,
check=False,
)
def command_exists(vmid: int, name: str) -> bool:
return pct(vmid, ["sh", "-c", f"command -v {name} >/dev/null 2>&1"]).returncode == 0
def docker_version(vmid: int) -> str:
result = pct(vmid, ["docker", "version", "--format", "{{.Server.Version}}"], capture=True)
return result.stdout.strip() if result.returncode == 0 else ""
def apt_installed(vmid: int) -> list[str]:
installed: list[str] = []
for package in APT_PACKAGES:
result = pct(
vmid,
["dpkg-query", "-W", "-f=${db:Status-Abbrev}", package],
capture=True,
)
if result.returncode == 0 and result.stdout.startswith("ii"):
installed.append(package)
return installed
def apk_installed(vmid: int) -> list[str]:
return [package for package in APK_PACKAGES if pct(vmid, ["apk", "info", "-e", package], capture=True).returncode == 0]
def rpm_installed(vmid: int) -> list[str]:
return [package for package in RPM_PACKAGES if pct(vmid, ["rpm", "-q", package], capture=True).returncode == 0]
def resolve_method(vmid: int) -> tuple[str, list[str]]:
if command_exists(vmid, "apt-get") and command_exists(vmid, "dpkg-query"):
return "apt", apt_installed(vmid)
if command_exists(vmid, "apk"):
return "apk", apk_installed(vmid)
if command_exists(vmid, "dnf") and command_exists(vmid, "rpm"):
return "dnf", rpm_installed(vmid)
if command_exists(vmid, "snap") and pct(vmid, ["snap", "list", "docker"], capture=True).returncode == 0:
return "snap", ["docker"]
return "unsupported", []
def run_update(vmid: int, method: str, packages: list[str]) -> int:
if method == "apt":
if pct(vmid, ["apt-get", "update"]).returncode != 0:
return 1
return pct(
vmid,
[
"env",
"DEBIAN_FRONTEND=noninteractive",
"apt-get",
"-y",
"-o",
"Dpkg::Options::=--force-confold",
"install",
"--only-upgrade",
*packages,
],
).returncode
if method == "apk":
if pct(vmid, ["apk", "update"]).returncode != 0:
return 1
return pct(vmid, ["apk", "upgrade", "--no-cache", *packages]).returncode
if method == "dnf":
return pct(vmid, ["dnf", "-y", "upgrade", *packages]).returncode
if method == "snap":
return pct(vmid, ["snap", "refresh", "docker"]).returncode
return 1
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--vmid", required=True, type=int)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if args.vmid <= 0:
parser.error("--vmid must be a positive integer")
before = docker_version(args.vmid)
if not before:
print("ERROR: Docker Engine is not running or was not detected in this container.", file=sys.stderr)
return 2
method, packages = resolve_method(args.vmid)
if method == "unsupported" or not packages:
print(
"ERROR: Docker was detected, but no supported packaged installation was found. "
"Configure a custom update command for this installation.",
file=sys.stderr,
)
return 3
print(f"Docker Engine before: {before}")
print(f"Update method: {method}")
print("Installed Docker stack: " + ", ".join(packages))
if args.dry_run:
print("Dry run: no packages were changed.")
return 0
print("--- Updating only the installed Docker Engine stack ---")
if run_update(args.vmid, method, packages) != 0:
print("ERROR: the Docker package update failed.", file=sys.stderr)
return 4
after = ""
for _ in range(12):
after = docker_version(args.vmid)
if after:
break
time.sleep(1)
if not after:
print("ERROR: Docker did not become available again after the package update.", file=sys.stderr)
return 5
print(f"Docker Engine after: {after}")
if after == before:
print("Docker Engine was already at the newest package version available.")
else:
print("Docker Engine updated successfully.")
return 0
if __name__ == "__main__":
raise SystemExit(main())