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
@@ -655,6 +655,23 @@ def build_catalog(
release, release_candidates, release_error = select_release_candidate( release, release_candidates, release_error = select_release_candidate(
slug, app_name, launcher, installer slug, app_name, launcher, installer
) )
# Some current helpers do not install through
# fetch_and_deploy_gh_release, but explicitly write the release to
# the same marker used by check_for_gh_release. Actual Budget is a
# representative example (npm install + ~/.actualbudget). Recover
# the repository from the release check only when that marker maps
# to one unique candidate; never guess it from a generic header.
marker_release_repo: str | None = None
if len(relevant_markers) == 1:
marker_path = f"/root/{relevant_markers[0]}".lower()
marker_repos = {
str(candidate.get("repo") or "")
for candidate in release_candidates
if str(candidate.get("cache_file") or "").lower() == marker_path
and candidate.get("repo")
}
if len(marker_repos) == 1:
marker_release_repo = marker_repos.pop()
install_only_release = select_install_only_release( install_only_release = select_install_only_release(
slug, app_name, installer, header_repos slug, app_name, installer, header_repos
) if release is None else None ) if release is None else None
@@ -708,13 +725,13 @@ def build_catalog(
} }
continue continue
if len(relevant_markers) == 1 and helper_repo and helper_version: if len(relevant_markers) == 1 and marker_release_repo:
marker = relevant_markers[0] marker = relevant_markers[0]
hint = { hint = {
"installed_via": "file", "installed_via": "file",
"file_path": f"/root/{marker}", "file_path": f"/root/{marker}",
"file_regex": version_regex, "file_regex": version_regex,
"repo": helper_repo, "repo": marker_release_repo,
"github_source": "releases", "github_source": "releases",
"tag_regex": version_regex, "tag_regex": version_regex,
} }
@@ -745,7 +762,7 @@ def build_catalog(
records.append(record) records.append(record)
v2_apps[slug] = { v2_apps[slug] = {
"name": app_name, "name": app_name,
"repo": helper_repo, "repo": marker_release_repo,
"official_sources": official_sources, "official_sources": official_sources,
"detectors": v2_detectors, "detectors": v2_detectors,
} }
@@ -905,13 +922,14 @@ def demote_generic_helper_markers(
v2: dict[str, Any], v2: dict[str, Any],
audit: dict[str, Any], audit: dict[str, Any],
) -> list[str]: ) -> list[str]:
"""Remove generic /root/.app caches from the operational catalog. """Remove modern helper markers when compatibility-only output is wanted.
Even when install and update scripts both write the marker, it records Current community-scripts installers maintain ``/root/.<app>`` as their
helper/update state rather than interrogating the installed application. version contract, so these are valid detectors for modern helper-owned
Runtime checks also found these files absent on legacy and manually containers. They are not universal: legacy helpers and official/manual
updated LXC. They remain useful candidates/fallbacks in v2, not verified installs may not have them. The default conservative mode therefore keeps
primary detectors. them in v2; production generation can opt in with
``--include-helper-markers`` and the runtime reports their distinct source.
""" """
demoted: list[str] = [] demoted: list[str] = []
apps = v2.get("apps", {}) apps = v2.get("apps", {})
@@ -927,12 +945,12 @@ def demote_generic_helper_markers(
continue continue
catalog.pop(slug, None) catalog.pop(slug, None)
record["status"] = "candidate" record["status"] = "candidate"
record["reason"] = "generic helper marker is not guaranteed on legacy/manual installations" record["reason"] = "modern helper marker is not guaranteed on legacy/manual installations"
for detector in (apps.get(slug) or {}).get("detectors", []): for detector in (apps.get(slug) or {}).get("detectors", []):
if detector.get("installed_via") == "file" and detector.get("file_path") == path: if detector.get("installed_via") == "file" and detector.get("file_path") == path:
detector["verification"] = "candidate-helper-marker" detector["verification"] = "candidate-helper-marker"
detector["limitation"] = ( detector["limitation"] = (
"Observed absent on legacy/manual LXC; use only as fallback or after runtime probe" "Valid for modern helper installs; absent on legacy/manual LXC"
) )
demoted.append(slug) demoted.append(slug)
return sorted(demoted) return sorted(demoted)
@@ -1147,14 +1165,16 @@ def apply_runtime_overrides(
"""Apply detectors proven against real containers. """Apply detectors proven against real containers.
The generated/static catalog is intentionally conservative. This optional The generated/static catalog is intentionally conservative. This optional
overlay promotes only detectors carrying runtime evidence. Unsupported overlay promotes only detectors carrying runtime evidence. The compatible
future methods (for example ``python_dist`` or ``docker_label``) are kept catalog now supports every detector implemented by ``lxc_apps.py``;
in v2 but are not written to the current-compatible v1 catalog. retaining an older dpkg/file/binary-only allow-list silently discarded
proven Python and Docker detectors.
""" """
result: dict[str, Any] = { result: dict[str, Any] = {
"file": str(overrides_path) if overrides_path else None, "file": str(overrides_path) if overrides_path else None,
"promoted_to_v1": [], "promoted_to_v1": [],
"v2_only": [], "v2_only": [],
"runtime_only": [],
"invalid": [], "invalid": [],
} }
if overrides_path is None or not overrides_path.is_file(): if overrides_path is None or not overrides_path.is_file():
@@ -1164,13 +1184,17 @@ def apply_runtime_overrides(
if not isinstance(apps_raw, dict): if not isinstance(apps_raw, dict):
raise CatalogError("runtime overrides must contain an 'apps' object") raise CatalogError("runtime overrides must contain an 'apps' object")
supported_v1 = {"dpkg", "apk", "file", "binary"} supported_v1 = {
"dpkg", "apk", "file", "binary", "python_dist",
"docker_label", "docker_exec", "command", "manual",
}
v2_apps = v2.get("apps", {}) v2_apps = v2.get("apps", {})
detector_keys = { detector_keys = {
"installed_via", "package", "file_path", "file_regex", "installed_via", "package", "file_path", "file_regex",
"binary_path", "binary_args", "python_path", "distribution", "binary_path", "binary_args", "python_path", "distribution",
"container_name", "label", "repo", "github_source", "tag_regex", "container_name", "label", "command_argv", "installed_version",
"installed_regex", "repo", "github_source", "tag_regex", "installed_regex",
"upstream_type", "upstream_url", "upstream_json_path", "docker_image",
} }
passthrough_keys = { passthrough_keys = {
"file_fallbacks", "alt_detectors", "default_ports", "logo", "website", "file_fallbacks", "alt_detectors", "default_ports", "logo", "website",
@@ -1193,8 +1217,43 @@ def apply_runtime_overrides(
} }
app = v2_apps.get(slug) app = v2_apps.get(slug)
if not isinstance(app, dict): if not isinstance(app, dict):
result["invalid"].append(slug) # Official/manual and nested Docker applications do not
continue # necessarily have a community-scripts ct/<slug>.sh launcher.
# A runtime-proven override is sufficient to create their v2
# entry; rejecting it here silently reintroduced the old
# helper-only limitation.
display_name = str(spec.get("name") or "").strip()
if not display_name:
display_name = slug.replace("-", " ").replace("_", " ").title()
app = {
"name": display_name,
"repo": detector.get("repo"),
"official_sources": [],
"detectors": [],
}
v2_apps[slug] = app
result["runtime_only"].append(slug)
# Preserve every statically-proven modern community-scripts marker
# when a stronger runtime detector becomes primary. This gives new
# helper installs their official /root/.<app> checker while keeping
# package/binary/python detectors first for legacy/manual installs.
helper_markers: list[dict[str, str]] = []
for candidate in app.get("detectors", []):
marker_path = candidate.get("file_path")
marker_regex = candidate.get("file_regex")
if (
candidate.get("installed_via") == "file"
and isinstance(marker_path, str)
and re.fullmatch(r"/root/\.[A-Za-z0-9_.-]+", marker_path)
and isinstance(marker_regex, str)
and marker_regex
):
helper_markers.append({
"path": marker_path,
"regex": marker_regex,
"source": "helper_marker",
})
app.setdefault("detectors", []).insert(0, v2_detector) app.setdefault("detectors", []).insert(0, v2_detector)
app["runtime_evidence"] = evidence app["runtime_evidence"] = evidence
@@ -1211,20 +1270,35 @@ def apply_runtime_overrides(
for key, value in presentation_source.items() for key, value in presentation_source.items()
if key in {"default_ports", "logo", "website"} if key in {"default_ports", "logo", "website"}
} }
hint = {k: v for k, v in detector.items() if k not in { # Every method-specific field is required at runtime. The old
"binary_args", "python_path", "distribution", "container_name", # compatibility filter removed python_path/distribution,
"label", "installed_regex", # binary_args and container fields, producing catalog entries
}} # that validated statically but could never execute.
hint = dict(detector)
for key in passthrough_keys: for key in passthrough_keys:
if key in spec: if key in spec:
hint[key] = spec[key] hint[key] = spec[key]
fallbacks = [
dict(item) for item in hint.get("file_fallbacks", [])
if isinstance(item, dict)
]
known_paths = {
item.get("path") for item in fallbacks if isinstance(item.get("path"), str)
}
primary_path = hint.get("file_path") if hint.get("installed_via") == "file" else None
for marker in helper_markers:
if marker["path"] != primary_path and marker["path"] not in known_paths:
fallbacks.append(marker)
known_paths.add(marker["path"])
if fallbacks:
hint["file_fallbacks"] = fallbacks
hint.update(presentation) hint.update(presentation)
catalog[slug] = hint catalog[slug] = hint
result["promoted_to_v1"].append(slug) result["promoted_to_v1"].append(slug)
else: else:
result["v2_only"].append(slug) result["v2_only"].append(slug)
for key in ("promoted_to_v1", "v2_only", "invalid"): for key in ("promoted_to_v1", "v2_only", "runtime_only", "invalid"):
result[key].sort() result[key].sort()
return result return result
@@ -49,12 +49,15 @@ jobs:
# `--runtime-overrides` folds real-CT evidence into the # `--runtime-overrides` folds real-CT evidence into the
# operational hints (canonical paths, cross-method fallbacks # operational hints (canonical paths, cross-method fallbacks
# per app) so the runtime doesn't get fed helper-marker # per app) so the runtime doesn't get fed helper-marker
# false-positives. # false-positives. Modern helper markers are included as their
# official version contract; runtime keeps them distinguishable from
# canonical package/binary/manual detectors for legacy compatibility.
run: | run: |
python .github/scripts/generate_app_tracking_catalog.py \ python .github/scripts/generate_app_tracking_catalog.py \
--helpers-cache json/helpers_cache.json \ --helpers-cache json/helpers_cache.json \
--existing json/app_tracking_hints.json \ --existing json/app_tracking_hints.json \
--runtime-overrides json/runtime_verified_overrides.json \ --runtime-overrides json/runtime_verified_overrides.json \
--include-helper-markers \
--output json/app_tracking_hints.generated.json \ --output json/app_tracking_hints.generated.json \
--v2-output /tmp/app_tracking_catalog.v2.json \ --v2-output /tmp/app_tracking_catalog.v2.json \
--audit-output /tmp/app_tracking_hints.audit.json --audit-output /tmp/app_tracking_hints.audit.json
@@ -96,8 +99,9 @@ jobs:
# governs generator-covered slugs. # governs generator-covered slugs.
GENERATOR_FIELDS = { GENERATOR_FIELDS = {
"installed_via", "package", "file_path", "file_regex", "installed_via", "package", "file_path", "file_regex",
"binary_path", "repo", "github_source", "tag_regex", "binary_path", "binary_args", "python_path", "distribution",
"installed_regex", "container_name", "label", "command_argv", "installed_version",
"repo", "github_source", "tag_regex", "installed_regex",
# Upstream source discriminator + per-type fields # Upstream source discriminator + per-type fields
# (http_json + docker_hub). Kept in the whitelist so a # (http_json + docker_hub). Kept in the whitelist so a
# curated entry in runtime_verified_overrides.json can # curated entry in runtime_verified_overrides.json can
+518 -81
View File
@@ -10,10 +10,9 @@
* • an optional GitHub repo for upstream version tracking * • an optional GitHub repo for upstream version tracking
* • a list of ports, each with a description and web path * • a list of ports, each with a description and web path
* *
* Docker apps are "register-only": they exist to produce clickable * Docker image updates live exclusively in the Updates tab. The App
* links, ProxMenux does NOT try to track their version and NEVER * tab only registers the Docker engine/app identity and shows installed
* emits warnings for them — updates for Docker apps are handled by * metadata, so an unregistered detection can never create update noise.
* Docker itself.
* *
* For ProxMenux-managed OCI CTs (Secure Gateway) the panel is * For ProxMenux-managed OCI CTs (Secure Gateway) the panel is
* read-only — the actual update lifecycle lives in Security → * read-only — the actual update lifecycle lives in Security →
@@ -24,8 +23,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"
import { import {
Loader2, Save, RefreshCw, Trash2, Package, ExternalLink, Loader2, Save, RefreshCw, Trash2, Package, ExternalLink,
AlertTriangle, Info, PlusCircle, Pencil, ChevronDown, ChevronRight, EyeOff, AlertTriangle, Info, PlusCircle, Pencil, ChevronDown, ChevronRight, EyeOff,
ArrowUpCircle, RotateCcw, Check, Settings2, ShieldCheck, CheckCircle2, ArrowUpCircle, RotateCcw, Check, Settings2, ShieldCheck,
Bell, BellOff, Bell, BellOff, Search,
} from "lucide-react" } from "lucide-react"
import { Card, CardContent } from "./ui/card" import { Card, CardContent } from "./ui/card"
import { Button } from "./ui/button" import { Button } from "./ui/button"
@@ -34,7 +33,7 @@ import { Label } from "./ui/label"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config" 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" import { useT } from "@/lib/i18n/provider"
// installed_via is optional now — an empty value means "register only, // installed_via is optional now — an empty value means "register only,
@@ -84,6 +83,11 @@ interface AppConfig {
health_path?: string health_path?: string
logo_url?: string logo_url?: string
helper_slug?: 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. // Per-app opt-out for the `app_update_available` notification.
// Absent / true = notify; false = silenced. Set from the bell // Absent / true = notify; false = silenced. Set from the bell
// toggle on each app card and/or the Edit form's checkbox. // toggle on each app card and/or the Edit form's checkbox.
@@ -104,6 +108,7 @@ interface DetectedApp {
interface AppState { interface AppState {
installed_version: string | null installed_version: string | null
latest_version: string | null latest_version: string | null
latest_published_at?: string | null
update_available: boolean | null update_available: boolean | null
error: string | null error: string | null
checked_at: string | null checked_at: string | null
@@ -123,18 +128,77 @@ interface SidecarResponse {
updated_at?: string 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 { interface TrackingSuggestion {
installed_via: "dpkg" | "apk" | "file" | "binary" installed_via: Exclude<InstalledVia, "">
package?: string package?: string
file_path?: string file_path?: string
file_regex?: string file_regex?: string
binary_path?: 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 repo?: string
github_source?: "releases" | "tags" github_source?: "releases" | "tags"
tag_regex?: string 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 { interface Suggestions {
ready?: boolean
name_suggestion: string | null name_suggestion: string | null
helper_slug: string | null helper_slug: string | null
port_suggestions: number[] port_suggestions: number[]
@@ -143,6 +207,7 @@ interface Suggestions {
default_ports?: number[] default_ports?: number[]
logo_url?: string | null logo_url?: string | null
extras?: DetectedApp[] extras?: DetectedApp[]
docker_web_links?: DockerWebLinkSuggestion[]
} }
// Compact catalog entry — one row for every registerable app the // Compact catalog entry — one row for every registerable app the
@@ -209,6 +274,44 @@ const EMPTY_APP: AppConfig = {
logo_url: "", 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 // Default scheme heuristic for freshly-added ports — only used to
// pre-select the dropdown. The user always has the final say via // pre-select the dropdown. The user always has the final say via
// the http/https selector next to the port input. // 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 [sidecar, setSidecar] = useState<SidecarResponse | null>(seed?.sidecar ?? null)
const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null) const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null)
const [error, setError] = useState<string | null>(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 // Editor state
const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null) const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [testingDetector, setTestingDetector] = useState(false)
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
const [busyAppId, setBusyAppId] = useState<string | null>(null) const [busyAppId, setBusyAppId] = useState<string | null>(null)
// Advanced section (version tracking) is collapsed by default so the // Advanced section (version tracking) is collapsed by default so the
// basic Name + Ports flow stays approachable. Auto-expanded when // 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 // restore, this panel is skipped entirely and the button opens the
// editor directly (fast path for the common case). // editor directly (fast path for the common case).
const [browseOpen, setBrowseOpen] = useState(false) 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 // Global "manage apps" mode. When ON, every app card grows a footer
// with Remove / Check / Edit fields actions. When OFF the cards are // with Remove / Check / Edit fields actions. When OFF the cards are
// pure info — the only surfaced action is a hover-reveal Check icon // pure info; detector checks remain available after enabling Edit.
// on the LATEST UPSTREAM panel. Toggled from a single button next to // Toggled from a single button next to
// "Add another application". // "Add another application".
const [editMode, setEditMode] = useState(false) const [editMode, setEditMode] = useState(false)
@@ -309,6 +419,42 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
useEffect(() => { load() }, [load]) 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 // Turn a raw backend error string ("network error: The read operation
// timed out", etc.) into a localized message. Upstream check errors // timed out", etc.) into a localized message. Upstream check errors
// are surfaced verbatim by `lxc_apps.py:_fetch_upstream()`, and the // 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 return msg
} }
// Fetch the picker catalog once per panel mount. Best-effort — if const editorOpen = !!editing
// the API is unreachable, the picker just stays empty and users
// type the app name manually (same as before this feature). // The picker catalog is only needed after the user opens the editor.
useEffect(() => { useEffect(() => {
if (!editorOpen || catalog.length > 0) return
let cancelled = false let cancelled = false
fetchApi("/api/apps/catalog") fetchApi<CatalogEntry[]>("/api/apps/catalog")
.then((data: CatalogEntry[]) => { .then((data: CatalogEntry[]) => {
if (!cancelled && Array.isArray(data)) setCatalog(data) if (!cancelled && Array.isArray(data)) setCatalog(data)
}) })
.catch(() => { /* non-fatal */ }) .catch(() => { /* non-fatal */ })
return () => { cancelled = true } return () => { cancelled = true }
}, []) }, [editorOpen, catalog.length])
// Derived state — computed here BEFORE any conditional early // Derived state — computed here BEFORE any conditional early
// return so React sees the same hook order on every render. // 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. // different-app panel. Not affected by registration state.
const hiddenDetections = detectedList.filter((d) => dismissedSlugs.has(d.slug)) 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, // "Register a different app" behavior: if there are hidden slugs,
// surface them first (with Restore) so the user can bring one back // surface them first (with Restore) so the user can bring one back
// instead of typing everything by hand. If nothing to restore, go // 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 || "", health_path: existing.health_path || "",
logo_url: existing.logo_url || "", logo_url: existing.logo_url || "",
helper_slug: existing.helper_slug || "", 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, notifications_enabled: existing.notifications_enabled !== false,
exclude_from_badge: existing.exclude_from_badge === true, 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.name = p.name
seed.logo_url = p.logo_url || "" seed.logo_url = p.logo_url || ""
seed.helper_slug = p.slug 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) => ({ seed.ports = p.default_ports.map((port) => ({
port, port,
scheme: defaultSchemeFor(port), scheme: defaultSchemeFor(port),
@@ -477,6 +670,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
file_path: t.file_path || "", file_path: t.file_path || "",
file_regex: t.file_regex || "", file_regex: t.file_regex || "",
binary_path: t.binary_path || "", 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" : ""), upstream_type: (t as any).upstream_type || (t.repo ? "github" : ""),
repo: t.repo || "", repo: t.repo || "",
github_source: t.github_source || "releases", 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 }) setEditing({ appId: existing?.id || null, draft: seed })
setDetectorTest(null)
setError(null) setError(null)
}, [suggestions, vmid, sidecar]) }, [suggestions, vmid, sidecar])
const closeEditor = () => { const closeEditor = () => {
setEditing(null) setEditing(null)
setDetectorTest(null)
setError(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) if ((r as any).error) throw new Error((r as any).error)
setSidecar(r) setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
setEditing(null) setEditing(null)
onChange?.() onChange?.()
} catch (e: any) { } 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) => { const checkOne = async (appId: string) => {
setBusyAppId(appId) setBusyAppId(appId)
setError(null) setError(null)
@@ -539,7 +761,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
method: "POST", method: "POST",
}) })
setSidecar(r) setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
onChange?.() onChange?.()
} catch (e: any) { } catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.checkFailed")) setError(e?.message || t("vmLxc.appEditor.checkFailed"))
@@ -565,7 +787,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
body: JSON.stringify(payload), body: JSON.stringify(payload),
}) })
setSidecar(r) setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
onChange?.() onChange?.()
} catch (e: any) { } catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.saveFailed")) setError(e?.message || t("vmLxc.appEditor.saveFailed"))
@@ -579,10 +801,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
setBusyAppId(appId) setBusyAppId(appId)
setError(null) setError(null)
try { try {
await fetchApi(`/api/vms/${vmid}/apps/${appId}`, { method: "DELETE" }) const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps/${appId}`, { method: "DELETE" })
// Reload from server so the empty state re-fetches suggestions setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
await load()
onChange?.() onChange?.()
} catch (e: any) { } catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.deleteFailed")) 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 }), body: JSON.stringify({ slug, dismissed: true }),
}) })
setSidecar(r) setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
} catch (e: any) { } catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.dismissFailed")) setError(e?.message || t("vmLxc.appEditor.dismissFailed"))
await load() // resync on failure await load() // resync on failure
@@ -631,7 +852,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
body: JSON.stringify({ slug, dismissed: false }), body: JSON.stringify({ slug, dismissed: false }),
}) })
setSidecar(r) setSidecar(r)
invalidateLxcApps(vmid) setLxcAppsCached(vmid, r, suggestions)
} catch (e: any) { } catch (e: any) {
setError(e?.message || t("vmLxc.appEditor.restoreFailed")) setError(e?.message || t("vmLxc.appEditor.restoreFailed"))
await load() await load()
@@ -666,7 +887,6 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const repo = isSecureGateway ? "tailscale/tailscale" : "" const repo = isSecureGateway ? "tailscale/tailscale" : ""
const methodLine = isSecureGateway ? `apk · tailscale · ${t("vmLxc.appEditor.managedStatus")}` : t("vmLxc.appEditor.managedStatus") const methodLine = isSecureGateway ? `apk · tailscale · ${t("vmLxc.appEditor.managedStatus")}` : t("vmLxc.appEditor.managedStatus")
const hasUpdate = managed.update_available === true const hasUpdate = managed.update_available === true
const upToDate = managed.update_available === false && !!managed.installed_version
const showVersions = !!(managed.installed_version || managed.latest_version || repo) const showVersions = !!(managed.installed_version || managed.latest_version || repo)
return ( return (
@@ -756,16 +976,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div> </div>
)} )}
{showVersions && ( {showVersions && (managed.installed_version || repo) && (
<div className={"mt-3 grid gap-3 " + (repo ? "grid-cols-2" : "grid-cols-1")}> <div className={"mt-3 grid gap-3 " + (managed.installed_version && repo ? "grid-cols-2" : "grid-cols-1")}>
{managed.installed_version && ( {managed.installed_version && (
<div className="p-3 rounded-md bg-muted/40"> <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-[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} {managed.installed_version}
{upToDate && (
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
)}
</div> </div>
</div> </div>
)} )}
@@ -818,8 +1035,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const draft = editing.draft const draft = editing.draft
const method = draft.installed_via || "" const method = draft.installed_via || ""
const isPackaged = method === "dpkg" || method === "apk" const isPackaged = method === "dpkg" || method === "apk"
const setField = (patch: Partial<AppConfig>) => const setField = (patch: Partial<AppConfig>) => {
setDetectorTest(null)
setEditing({ ...editing, draft: { ...draft, ...patch } }) setEditing({ ...editing, draft: { ...draft, ...patch } })
}
// Editing the Name auto-fills the Package field on packaged // Editing the Name auto-fills the Package field on packaged
// methods when it's still empty. Rationale: 90% of the time the // methods when it's still empty. Rationale: 90% of the time the
// dpkg/apk package name mirrors the friendly app name (jellyfin, // 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 }] }) 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) => const removePort = (i: number) =>
setField({ ports: draft.ports.filter((_, idx) => idx !== i) }) setField({ ports: draft.ports.filter((_, idx) => idx !== i) })
const usedPorts = new Set(draft.ports.map((p) => p.port)) 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -908,13 +1154,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
onClick={async () => { onClick={async () => {
setPickerOpen(false) setPickerOpen(false)
try { 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. // Seed the entire form from the picker detail.
const patch: Partial<AppConfig> = { const patch: Partial<AppConfig> = {
name: detail.name, name: detail.name,
helper_slug: detail.slug, helper_slug: detail.slug,
logo_url: detail.logo_url || "", logo_url: detail.logo_url || "",
ports: detail.default_ports?.length ports: detail.slug !== "docker" && detail.default_ports?.length
? detail.default_ports.map((p) => ({ ? detail.default_ports.map((p) => ({
port: p, port: p,
scheme: defaultSchemeFor(p), scheme: defaultSchemeFor(p),
@@ -1002,6 +1248,53 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</Button> </Button>
</div> </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 {/* Detected chips FIRST — one-click add. Only shown when
there are chips left to suggest, so empty states stay there are chips left to suggest, so empty states stay
clean. Click on a chip: fills the current empty row clean. Click on a chip: fills the current empty row
@@ -1024,7 +1317,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div> </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"> <div className="text-xs text-muted-foreground italic">
{t("vmLxc.appEditor.noWebPorts")} {t("vmLxc.appEditor.noWebPorts")}
</div> </div>
@@ -1470,9 +1763,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
maxLength={255} maxLength={255}
/> />
<div className="text-[10px] text-muted-foreground mt-1"> <div className="text-[10px] text-muted-foreground mt-1">
<code className="text-foreground/70">owner/name</code> or bare {t("vmLxc.appEditor.dockerVersionedTagsHelp")}
name for official images. ProxMenux picks the highest semver tag
matching the filter below.
</div> </div>
</div> </div>
)} )}
@@ -1489,13 +1780,82 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
placeholder={t("vmLxc.appEditor.tagRegexPlaceholder")} placeholder={t("vmLxc.appEditor.tagRegexPlaceholder")}
className="font-mono text-xs" 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"> <div className="text-[10px] text-muted-foreground mt-1">
{upstreamType === "github" && "Extracts the version from the release tag name."} {upstreamType === "github" && "Extracts the version from the release tag name."}
{upstreamType === "http_json" && "Optional — extract a substring from the endpoint's value."} {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>
</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> </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 && ( {error && (
<div className="text-xs text-red-400 flex items-start gap-1.5"> <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" /> <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"> <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 <Button
onClick={save} onClick={save}
disabled={saving || !draft.name.trim()} disabled={saving || testingDetector || !draft.name.trim()}
className="bg-blue-500 hover:bg-blue-600 text-white" 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" />} {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 flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
{d.logo_url && ( {d.logo_url && (
<img <ThemeAwareLogo
src={d.logo_url} src={d.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain" 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="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 flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
{d.logo_url && ( {d.logo_url && (
<img <ThemeAwareLogo
src={d.logo_url} src={d.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain" 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="min-w-0">
<div className="text-sm font-semibold text-foreground truncate">{d.name}</div> <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> </div>
<div className="flex flex-row gap-2 flex-shrink-0 sm:justify-end w-full sm:w-auto"> <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)} {visibleDetected.map(renderDetectionChip)}
</div> </div>
)} )}
<div className="pt-1 flex justify-center"> <div className="pt-1 flex flex-wrap justify-center gap-2">
<Button onClick={openBrowseOrEditor} variant={visibleDetected.length > 0 ? "outline" : "default"} <Button
className={visibleDetected.length > 0 ? "" : "bg-blue-500 hover:bg-blue-600 text-white"}> 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" /> <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 && ( {hiddenDetections.length > 0 && (
<span className="ml-2 text-[10px] opacity-70"> <span className="ml-2 text-[10px] opacity-70">
· {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })} · {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })}
@@ -1728,6 +2160,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)} )}
</Button> </Button>
</div> </div>
{detectionNotice && (
<p className={`text-xs text-center ${detectionNotice.found ? "text-emerald-400" : "text-muted-foreground"}`}>
{detectionNotice.text}
</p>
)}
</CardContent> </CardContent>
</Card> </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 // Version tracking is on when installed_via is set. Without a
// method the app is register-only — no cards, no warnings. // method the app is register-only — no cards, no warnings.
const tracking = !!app.installed_via const tracking = !!app.installed_via
const hasUpdate = tracking && st?.update_available === true
const upToDate = tracking && st?.update_available === false && !!st?.installed_version
return ( return (
<Card key={app.id} className="border border-border bg-card/50"> <Card key={app.id} className="border border-border bg-card/50">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between gap-3 mb-3"> <div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
{app.logo_url && ( {app.logo_url && (
<img <ThemeAwareLogo
src={app.logo_url} src={app.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain" 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"> <div className="min-w-0 flex-1">
@@ -1801,30 +2234,17 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
)} )}
</div> </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 hasUpstream = !!(app.repo || app.upstream_type)
const hasUpdate = st?.update_available === true
if (!tracking || !(st?.installed_version || hasUpstream)) return null if (!tracking || !(st?.installed_version || hasUpstream)) return null
return ( 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 && ( {st?.installed_version && (
<div className="p-3 rounded-md bg-muted/40"> <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-[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} {st.installed_version}
{upToDate && (
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
)}
</div> </div>
</div> </div>
)} )}
@@ -1867,11 +2287,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
return ( return (
<div key={p.port} className="flex items-start gap-3 min-w-0"> <div key={p.port} className="flex items-start gap-3 min-w-0">
{p.logo_url && ( {p.logo_url && (
<img <ThemeAwareLogo
src={p.logo_url} src={p.logo_url}
alt=""
className="h-14 w-14 flex-shrink-0 rounded-md object-contain" 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"> <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 {/* Footer with per-card actions — only rendered in the
global edit mode (toggled from the "Edit" button next global edit mode (toggled from the "Edit" button next
to Add another application). View mode keeps cards to Add another application). View mode keeps cards
chrome-free; Check is still reachable via the chrome-free; Check remains available in edit mode.
hover-reveal icon on the LATEST panel. Buttons match Buttons match
the Settings-page section style (h-8, outline, the Settings-page section style (h-8, outline,
small icon + label) for visual consistency across small icon + label) for visual consistency across
the app. */} 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- detections, so the user gets one-click Restore before hand-
typing a custom app. */} typing a custom app. */}
{apps.length > 0 && ( {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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -2019,6 +2450,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
</div> </div>
)} )}
{apps.length > 0 && detectionNotice && (
<p className={`text-xs text-right ${detectionNotice.found ? "text-emerald-400" : "text-muted-foreground"}`}>
{detectionNotice.text}
</p>
)}
{error && ( {error && (
<div className="text-xs text-red-400 flex items-start gap-1.5"> <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" /> <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 // 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) // virtual-machines.tsx (which prefetches on modal open and on hover)
// and lxc-app-panel.tsx (which reads the cache first and only fetches // and lxc-app-panel.tsx (which reads the cache first and only fetches
// if empty). The in-flight promise map dedups concurrent requests: if // 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 dataCache = new Map<number, LxcAppsBundle>()
const inFlight = new Map<number, Promise<LxcAppsBundle | null>>() const inFlight = new Map<number, Promise<LxcAppsBundle | null>>()
const cacheRevision = new Map<number, number>()
export function getLxcAppsCached(vmid: number): LxcAppsBundle | undefined { export function getLxcAppsCached(vmid: number): LxcAppsBundle | undefined {
return dataCache.get(vmid) 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> { export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
const existing = inFlight.get(vmid) const existing = inFlight.get(vmid)
if (existing) return existing if (existing) return existing
const startedRevision = cacheRevision.get(vmid) || 0
const p = Promise.all([ const p = Promise.all([
fetchApi(`/api/vms/${vmid}/apps`).catch(() => null) as Promise<any>, fetchApi(`/api/vms/${vmid}/apps`).catch(() => null) as Promise<any>,
fetchApi(`/api/vms/${vmid}/apps/suggestions`).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]) => { .then(([sc, sug]) => {
if (!sc) return null if (!sc) return null
const bundle: LxcAppsBundle = { sidecar: sc, suggestions: sug } 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) dataCache.set(vmid, bundle)
return bundle return bundle
}) })
@@ -42,17 +70,20 @@ export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
} }
export function invalidateLxcApps(vmid: number): void { export function invalidateLxcApps(vmid: number): void {
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
dataCache.delete(vmid) dataCache.delete(vmid)
} }
// Seed the cache with a sidecar payload from the bulk modal-cache // Seed the cache from the bulk modal-cache endpoint. Both registered
// endpoint. Only the sidecar side is populated — suggestions still // apps and startup detection suggestions are already in memory, so
// resolve lazily when the App panel actually mounts (the bulk // opening the App tab never starts a discovery scan.
// endpoint intentionally excludes them since most guests don't export function seedLxcAppsCache(
// need the auto-detected chips and the payload would balloon). vmid: number,
export function seedLxcAppsCache(vmid: number, sidecar: any): void { sidecar: any,
suggestions?: any | null,
): void {
if (!sidecar) return if (!sidecar) return
const existing = dataCache.get(vmid) const existing = dataCache.get(vmid)
if (existing) return // per-panel fetch already ran, don't overwrite 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", "targetBoth": "Betriebssystem + Anwendung",
"lastRun": "Letzte Ausführung: {date}", "lastRun": "Letzte Ausführung: {date}",
"runSuccess": "✓ Erfolg", "runSuccess": "✓ Erfolg",
"runPartial": "teilweise abgeschlossen",
"runFailed": "✗ fehlgeschlagen", "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", "notScheduled": "Nicht geplant",
"frequency": "Frequenz", "frequency": "Frequenz",
"cronExpression": "Cron-Ausdruck", "cronExpression": "Cron-Ausdruck",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Nur Betriebssystempakete", "targetOptionOs": "Nur Betriebssystempakete",
"targetOptionApp": "Nur Bewerbung", "targetOptionApp": "Nur Bewerbung",
"targetOptionBoth": "Betriebssystem + Anwendung", "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", "deleteButton": "Zeitplan löschen",
"deleteConfirm": "Geplante Updates für diesen Container entfernen? Die Standardeinstellungen (Sicherung + Neustart) werden beibehalten." "deleteConfirm": "Geplante Updates für diesen Container entfernen? Die Standardeinstellungen (Sicherung + Neustart) werden beibehalten."
}, },
@@ -1217,7 +1230,7 @@
"osPlusApp": "Betriebssystem + {appName}-Updates", "osPlusApp": "Betriebssystem + {appName}-Updates",
"osPlusApps": "Betriebssystem- und Apps-Updates", "osPlusApps": "Betriebssystem- und Apps-Updates",
"noUpdateMethodTitle": "Keine Aktualisierungsmethode verfügbar", "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", "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.", "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", "hideNoticeButton": "Diesen Hinweis für diese App ausblenden",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Anwendung", "applicationDefaultName": "Anwendung",
"installedLabel": "installiert", "installedLabel": "installiert",
"upToDateAtLabel": "Aktuell unter", "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.", "versionTrackingPending": "Versionsverfolgung ausstehend bei der nächsten geplanten Prüfung werden die installierte und die verfügbare Version angezeigt.",
"customCommandTitle": "Benutzerdefinierter Update-Befehl", "customCommandTitle": "Benutzerdefinierter Update-Befehl",
"customCommandBody": "Führen Sie einen benutzerdefinierten Shell-Befehl aus, um diese App im Container zu aktualisieren.", "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", "hideNoticeAction": "Diesen Hinweis für {appName} ausblenden",
"detectedByPrefix": "Erkannt von", "detectedByPrefix": "Erkannt von",
"editApp": "Bearbeiten", "editApp": "Bearbeiten",
"configureUpdater": "Konfigurieren",
"alsoDetectedTitle": "Außerdem in diesem Container entdeckt:", "alsoDetectedTitle": "Außerdem in diesem Container entdeckt:",
"detectedInline": "· erkannt von {method}", "detectedInline": "· erkannt von {method}",
"customCommandLabel": "Benutzerdefinierter Update-Befehl", "customCommandLabel": "Benutzerdefinierter Update-Befehl",
@@ -1245,9 +1260,9 @@
"removeButton": "Entfernen", "removeButton": "Entfernen",
"cancelButton": "Stornieren", "cancelButton": "Stornieren",
"saveButton": "Speichern", "saveButton": "Speichern",
"editCommandButton": "Befehl bearbeiten",
"wireUpCommandButton": "Fügen Sie einen benutzerdefinierten Aktualisierungsbefehl hinzu",
"versionTrackingPendingShort": "Versionsverfolgung steht aus.", "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.", "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.", "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", "hideForApp": "Hinweis für {appName} ausblenden",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Noch keine Update-Informationen prüfen Sie unter Sicherheit → Secure Gateway.", "noManagedUpdateInfo": "Noch keine Update-Informationen prüfen Sie unter Sicherheit → Secure Gateway.",
"ociTitle": "OCI-Image-Container", "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.", "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.", "dockerImagesTitle": "Docker-Images",
"helperDetectedTitle": "Es wurde ein Hilfsskript-Updater erkannt", "dockerAppTitle": "Docker",
"helperDetectedBody": "Die manuelle Anwendung ist am sichersten.", "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…", "postApplyChecking": "Aktualisierungsergebnis wird überprüft…",
"postApplyAllOk": "{count} Paket(e) erfolgreich angewendet nichts ausstehend.", "postApplyAllOk": "{count} Paket(e) erfolgreich angewendet nichts ausstehend.",
"postApplyNothingPending": "Nichts ausstehend alles ist auf dem neuesten Stand.", "postApplyNothingPending": "Nichts ausstehend alles ist auf dem neuesten Stand.",
"postApplyPartial": "{pending} Paket(e) stehen nach der Ausführung noch aus.", "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." "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": { "appEditor": {
"closePanel": "Panel schließen", "closePanel": "Panel schließen",
"cancelButton": "Stornieren", "cancelButton": "Stornieren",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Derzeit ausgeblendet wird wieder in der Erkennungsliste angezeigt", "hiddenBadge": "Derzeit ausgeblendet wird wieder in der Erkennungsliste angezeigt",
"registerDifferent": "Registrieren Sie eine andere App", "registerDifferent": "Registrieren Sie eine andere App",
"detectedInContainer": "Auf diesem Container erkannt", "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", "installedManaged": "Installiert und verwaltet von ProxMenux",
"nameLabel": "Name", "nameLabel": "Name",
"installedViaLabel": "Installiert über", "installedViaLabel": "Installiert über",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "z. B. data.version oder releases[0].tag_name", "jsonPathPlaceholder": "z. B. data.version oder releases[0].tag_name",
"dockerImageLabel": "Docker Hub-Bild", "dockerImageLabel": "Docker Hub-Bild",
"dockerImagePlaceholder": "z. B. linuxserver/plex oder nginx", "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)", "tagRegexLabel": "Tag-Regex (mit Capture-Gruppe)",
"tagRegexPlaceholder": "z.B. v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "z.B. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registrieren Sie eine benutzerdefinierte App", "registerCustom": "Registrieren Sie eine benutzerdefinierte App",
"noAppsTitle": "Keine Bewerbungen registriert", "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.", "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", "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.", "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", "saveFailed": "Speichern fehlgeschlagen",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana braucht", "binaryArgsHintGrafana": "Grafana braucht",
"loadFailed": "Die App-Konfiguration konnte nicht geladen werden", "loadFailed": "Die App-Konfiguration konnte nicht geladen werden",
"checkFailed": "Prüfung fehlgeschlagen", "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", "deleteFailed": "Das Löschen ist fehlgeschlagen",
"dismissFailed": "Die Erkennung konnte nicht verworfen werden", "dismissFailed": "Die Erkennung konnte nicht verworfen werden",
"restoreFailed": "Die Erkennung konnte nicht wiederhergestellt werden", "restoreFailed": "Die Erkennung konnte nicht wiederhergestellt werden",
@@ -1399,6 +1494,10 @@
"webLinks": "Weblinks", "webLinks": "Weblinks",
"addPort": "Port hinzufügen", "addPort": "Port hinzufügen",
"detectedPorts": "Im Container erkannte Ports klicken Sie zum 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.", "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)", "trackUpstream": "Verfügbare Version verfolgen (optional)",
"trackOff": "Aus nur Link", "trackOff": "Aus nur Link",
@@ -1416,7 +1515,7 @@
"checkButton": "Überprüfen", "checkButton": "Überprüfen",
"editFieldsButton": "Felder bearbeiten", "editFieldsButton": "Felder bearbeiten",
"alsoDetectedContainer": "Auch auf diesem Container erkannt", "alsoDetectedContainer": "Auch auf diesem Container erkannt",
"addAnotherApplication": "Fügen Sie eine weitere Anwendung hinzu", "addAnotherApplication": "Weitere Anwendung registrieren",
"doneButton": "Erledigt", "doneButton": "Erledigt",
"editButton": "Bearbeiten", "editButton": "Bearbeiten",
"upstreamErrorTimeout": "Netzwerk-Timeout beim Kontaktieren des Upstreams", "upstreamErrorTimeout": "Netzwerk-Timeout beim Kontaktieren des Upstreams",
+106 -7
View File
@@ -1162,7 +1162,15 @@
"targetBoth": "OS + application", "targetBoth": "OS + application",
"lastRun": "Last run: {date}", "lastRun": "Last run: {date}",
"runSuccess": "✓ success", "runSuccess": "✓ success",
"runPartial": "completed partially",
"runFailed": "✗ failed", "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", "notScheduled": "Not scheduled",
"frequency": "Frequency", "frequency": "Frequency",
"cronExpression": "Cron expression", "cronExpression": "Cron expression",
@@ -1171,6 +1179,11 @@
"targetOptionOs": "OS packages only", "targetOptionOs": "OS packages only",
"targetOptionApp": "Application only", "targetOptionApp": "Application only",
"targetOptionBoth": "OS + application", "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", "deleteButton": "Delete schedule",
"deleteConfirm": "Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept." "deleteConfirm": "Remove the scheduled updates for this container? Apply defaults (backup + restart) are kept."
}, },
@@ -1216,7 +1229,7 @@
"osPlusApp": "OS + {appName} updates", "osPlusApp": "OS + {appName} updates",
"osPlusApps": "OS + Apps updates", "osPlusApps": "OS + Apps updates",
"noUpdateMethodTitle": "No update method available", "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", "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.", "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", "hideNoticeButton": "Hide this notice for this app",
@@ -1229,6 +1242,7 @@
"applicationDefaultName": "Application", "applicationDefaultName": "Application",
"installedLabel": "installed", "installedLabel": "installed",
"upToDateAtLabel": "Up to date at", "upToDateAtLabel": "Up to date at",
"versionLabel": "version {version}",
"versionTrackingPending": "Version tracking pending — the next scheduled check will populate installed and upstream numbers.", "versionTrackingPending": "Version tracking pending — the next scheduled check will populate installed and upstream numbers.",
"customCommandTitle": "Custom update command", "customCommandTitle": "Custom update command",
"customCommandBody": "Run a user-defined shell command to update this app inside the container.", "customCommandBody": "Run a user-defined shell command to update this app inside the container.",
@@ -1237,6 +1251,7 @@
"hideNoticeAction": "Hide this notice for {appName}", "hideNoticeAction": "Hide this notice for {appName}",
"detectedByPrefix": "Detected by", "detectedByPrefix": "Detected by",
"editApp": "Edit", "editApp": "Edit",
"configureUpdater": "Configure",
"alsoDetectedTitle": "Also detected in this container:", "alsoDetectedTitle": "Also detected in this container:",
"detectedInline": "· detected by {method}", "detectedInline": "· detected by {method}",
"customCommandLabel": "Custom update command", "customCommandLabel": "Custom update command",
@@ -1244,9 +1259,9 @@
"removeButton": "Remove", "removeButton": "Remove",
"cancelButton": "Cancel", "cancelButton": "Cancel",
"saveButton": "Save", "saveButton": "Save",
"editCommandButton": "Edit command",
"wireUpCommandButton": "Add custom update command",
"versionTrackingPendingShort": "Version tracking pending.", "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.", "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.", "noMethodHideBody": "This app has no update method registered. Set one from the App tab or dismiss this notice.",
"hideForApp": "Hide notice for {appName}", "hideForApp": "Hide notice for {appName}",
@@ -1265,15 +1280,59 @@
"noManagedUpdateInfo": "No update information yet — check from Security → Secure Gateway.", "noManagedUpdateInfo": "No update information yet — check from Security → Secure Gateway.",
"ociTitle": "OCI image container", "ociTitle": "OCI image container",
"ociBody": "This container was created from an OCI (Docker) image. Update management for OCI containers is coming with the upcoming OCI install feature — updates will rebuild the container from a newer image tag rather than patching packages inside.", "ociBody": "This container was created from an OCI (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.", "dockerImagesTitle": "Docker images",
"helperDetectedTitle": "Detected a helper-scripts updater", "dockerAppTitle": "Docker",
"helperDetectedBody": "Applying manually is safest.", "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…", "postApplyChecking": "Verifying update result…",
"postApplyAllOk": "{count} package(s) applied successfully — nothing pending.", "postApplyAllOk": "{count} package(s) applied successfully — nothing pending.",
"postApplyNothingPending": "Nothing pending — everything is up to date.", "postApplyNothingPending": "Nothing pending — everything is up to date.",
"postApplyPartial": "{pending} package(s) still pending after the run.", "postApplyPartial": "{pending} package(s) still pending after the run.",
"postApplyPartialSubline": "{applied} applied. Some updates did not complete — review the terminal output above." "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": { "appEditor": {
"closePanel": "Close panel", "closePanel": "Close panel",
"cancelButton": "Cancel", "cancelButton": "Cancel",
@@ -1283,6 +1342,17 @@
"hiddenBadge": "Currently hidden — will re-appear in the detection list", "hiddenBadge": "Currently hidden — will re-appear in the detection list",
"registerDifferent": "Register a different app", "registerDifferent": "Register a different app",
"detectedInContainer": "Detected on this container", "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", "installedManaged": "Installed and managed by ProxMenux",
"nameLabel": "Name", "nameLabel": "Name",
"installedViaLabel": "Installed via", "installedViaLabel": "Installed via",
@@ -1339,6 +1409,17 @@
"jsonPathPlaceholder": "e.g., data.version or releases[0].tag_name", "jsonPathPlaceholder": "e.g., data.version or releases[0].tag_name",
"dockerImageLabel": "Docker Hub image", "dockerImageLabel": "Docker Hub image",
"dockerImagePlaceholder": "e.g., linuxserver/plex or nginx", "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)", "tagRegexLabel": "Tag regex (with capture group)",
"tagRegexPlaceholder": "e.g., v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "e.g., v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1375,6 +1456,13 @@
"noAppsTitle": "No applications registered", "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.", "noAppsBody": "Register each application running in this container. Get clickable web links, and — optionally — upstream version tracking + notifications for new releases.",
"registerApplication": "Register application", "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", "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.", "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", "saveFailed": "Save failed",
@@ -1385,6 +1473,13 @@
"binaryArgsHintGrafana": "Grafana needs", "binaryArgsHintGrafana": "Grafana needs",
"loadFailed": "Could not load app configuration", "loadFailed": "Could not load app configuration",
"checkFailed": "Check failed", "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", "deleteFailed": "Delete failed",
"dismissFailed": "Could not dismiss detection", "dismissFailed": "Could not dismiss detection",
"restoreFailed": "Could not restore detection", "restoreFailed": "Could not restore detection",
@@ -1401,6 +1496,10 @@
"webLinks": "Web links", "webLinks": "Web links",
"addPort": "Add port", "addPort": "Add port",
"detectedPorts": "Ports detected in the container — click to add:", "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.", "noWebPorts": "No web ports listening. Use Add port if you still want to create a link manually.",
"trackUpstream": "Track upstream version (optional)", "trackUpstream": "Track upstream version (optional)",
"trackOff": "Off — link only", "trackOff": "Off — link only",
@@ -1418,7 +1517,7 @@
"checkButton": "Check", "checkButton": "Check",
"editFieldsButton": "Edit fields", "editFieldsButton": "Edit fields",
"alsoDetectedContainer": "Also detected on this container", "alsoDetectedContainer": "Also detected on this container",
"addAnotherApplication": "Add another application", "addAnotherApplication": "Register another application",
"doneButton": "Done", "doneButton": "Done",
"editButton": "Edit", "editButton": "Edit",
"notificationsEnabled": "Upstream update notifications ON — click to mute", "notificationsEnabled": "Upstream update notifications ON — click to mute",
+108 -9
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "SO + aplicación", "targetBoth": "SO + aplicación",
"lastRun": "Última ejecución: {date}", "lastRun": "Última ejecución: {date}",
"runSuccess": "✓ éxito", "runSuccess": "✓ éxito",
"runPartial": "completada parcialmente",
"runFailed": "✗ falló", "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", "notScheduled": "No programado",
"frequency": "Frecuencia", "frequency": "Frecuencia",
"cronExpression": "expresión cron", "cronExpression": "expresión cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Solo paquetes de sistema operativo", "targetOptionOs": "Solo paquetes de sistema operativo",
"targetOptionApp": "Sólo aplicación", "targetOptionApp": "Sólo aplicación",
"targetOptionBoth": "SO + 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", "deleteButton": "Eliminar horario",
"deleteConfirm": "¿Eliminar las actualizaciones programadas para este contenedor? Se mantienen los valores predeterminados de aplicación (copia de seguridad + reinicio)." "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}", "osPlusApp": "SO + actualizaciones {appName}",
"osPlusApps": "Actualizaciones de SO y aplicaciones", "osPlusApps": "Actualizaciones de SO y aplicaciones",
"noUpdateMethodTitle": "No hay ningún método de actualización disponible", "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", "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.", "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", "hideNoticeButton": "Ocultar este aviso para esta aplicación",
@@ -1229,7 +1242,8 @@
"securityUpdatesLabel": "actualizaciones de seguridad", "securityUpdatesLabel": "actualizaciones de seguridad",
"applicationDefaultName": "Solicitud", "applicationDefaultName": "Solicitud",
"installedLabel": "instalado", "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.", "versionTrackingPending": "Seguimiento de versiones pendiente: la próxima comprobación programada mostrará las versiones instalada y disponible.",
"customCommandTitle": "Comando de actualización personalizado", "customCommandTitle": "Comando de actualización personalizado",
"customCommandBody": "Ejecute un comando de shell definido por el usuario para actualizar esta aplicación dentro del contenedor.", "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}", "hideNoticeAction": "Ocultar este aviso para {appName}",
"detectedByPrefix": "Detectado por", "detectedByPrefix": "Detectado por",
"editApp": "Editar", "editApp": "Editar",
"configureUpdater": "Configurar",
"alsoDetectedTitle": "También detectado en este contenedor:", "alsoDetectedTitle": "También detectado en este contenedor:",
"detectedInline": "· detectado por {method}", "detectedInline": "· detectado por {method}",
"customCommandLabel": "Comando de actualización personalizado", "customCommandLabel": "Comando de actualización personalizado",
@@ -1245,9 +1260,9 @@
"removeButton": "Eliminar", "removeButton": "Eliminar",
"cancelButton": "Cancelar", "cancelButton": "Cancelar",
"saveButton": "Guardar", "saveButton": "Guardar",
"editCommandButton": "Editar comando",
"wireUpCommandButton": "Agregar comando de actualización personalizado",
"versionTrackingPendingShort": "Seguimiento de versión pendiente.", "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.", "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.", "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}", "hideForApp": "Ocultar aviso para {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Aún no hay información de actualización: verifique desde Seguridad → Secure Gateway.", "noManagedUpdateInfo": "Aún no hay información de actualización: verifique desde Seguridad → Secure Gateway.",
"ociTitle": "Contenedor de imágenes OCI", "ociTitle": "Contenedor de imágenes OCI",
"ociBody": "Este contenedor se creó a partir de una imagen OCI (Docker). La gestión de actualizaciones para contenedores OCI viene con la próxima función de instalación de OCI: las actualizaciones reconstruirán el contenedor a partir de una etiqueta de imagen más nueva en lugar de aplicar parches a los paquetes internos.", "ociBody": "Este contenedor se creó 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.", "dockerImagesTitle": "Imágenes Docker",
"helperDetectedTitle": "Detectado un actualizador de scripts auxiliares", "dockerAppTitle": "Docker",
"helperDetectedBody": "La aplicación manual es la más segura.", "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…", "postApplyChecking": "Comprobando resultado de la actualización…",
"postApplyAllOk": "{count} paquete(s) aplicados correctamente — nada pendiente.", "postApplyAllOk": "{count} paquete(s) aplicados correctamente — nada pendiente.",
"postApplyNothingPending": "Nada pendiente — todo actualizado.", "postApplyNothingPending": "Nada pendiente — todo actualizado.",
"postApplyPartial": "{pending} paquete(s) siguen pendientes tras la ejecución.", "postApplyPartial": "{pending} paquete(s) siguen pendientes tras la ejecución.",
"postApplyPartialSubline": "{applied} aplicados. Algunas actualizaciones no finalizaron — revisa la salida del terminal." "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": { "appEditor": {
"closePanel": "Cerrar panel", "closePanel": "Cerrar panel",
"cancelButton": "Cancelar", "cancelButton": "Cancelar",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Actualmente oculto: volverá a aparecer en la lista de detección", "hiddenBadge": "Actualmente oculto: volverá a aparecer en la lista de detección",
"registerDifferent": "Registrar una aplicación diferente", "registerDifferent": "Registrar una aplicación diferente",
"detectedInContainer": "Detectado en este contenedor", "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", "installedManaged": "Instalado y administrado por ProxMenux",
"nameLabel": "Nombre", "nameLabel": "Nombre",
"installedViaLabel": "Instalado a través de", "installedViaLabel": "Instalado a través de",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "por ejemplo, data.version o lanzamientos[0].tag_name", "jsonPathPlaceholder": "por ejemplo, data.version o lanzamientos[0].tag_name",
"dockerImageLabel": "Imagen de Docker Hub", "dockerImageLabel": "Imagen de Docker Hub",
"dockerImagePlaceholder": "por ejemplo, linuxserver/plex o nginx", "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)", "tagRegexLabel": "Etiquetar expresiones regulares (con grupo de captura)",
"tagRegexPlaceholder": "por ejemplo, v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "por ejemplo, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1375,7 +1456,14 @@
"registerCustom": "Registrar una aplicación personalizada", "registerCustom": "Registrar una aplicación personalizada",
"noAppsTitle": "No hay aplicaciones registradas", "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.", "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", "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.", "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", "saveFailed": "Error al guardar",
@@ -1386,6 +1474,13 @@
"binaryArgsHintGrafana": "Grafana necesita", "binaryArgsHintGrafana": "Grafana necesita",
"loadFailed": "No se pudo cargar la configuración de la aplicación", "loadFailed": "No se pudo cargar la configuración de la aplicación",
"checkFailed": "Verificación fallida", "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", "deleteFailed": "Error al eliminar",
"dismissFailed": "No se pudo descartar la detección", "dismissFailed": "No se pudo descartar la detección",
"restoreFailed": "No se pudo restaurar la detección", "restoreFailed": "No se pudo restaurar la detección",
@@ -1402,6 +1497,10 @@
"webLinks": "Enlaces web", "webLinks": "Enlaces web",
"addPort": "Agregar puerto", "addPort": "Agregar puerto",
"detectedPorts": "Puertos detectados en el contenedor: haga clic para agregar:", "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.", "noWebPorts": "No hay puertos web escuchando. Utilice Agregar puerto si aún desea crear un enlace manualmente.",
"trackUpstream": "Seguir versión disponible (opcional)", "trackUpstream": "Seguir versión disponible (opcional)",
"trackOff": "Desactivado: solo enlace", "trackOff": "Desactivado: solo enlace",
@@ -1419,7 +1518,7 @@
"checkButton": "Controlar", "checkButton": "Controlar",
"editFieldsButton": "Editar campos", "editFieldsButton": "Editar campos",
"alsoDetectedContainer": "También detectado en este contenedor.", "alsoDetectedContainer": "También detectado en este contenedor.",
"addAnotherApplication": "Agregar otra aplicación", "addAnotherApplication": "Registrar otra aplicación",
"doneButton": "Hecho", "doneButton": "Hecho",
"editButton": "Editar", "editButton": "Editar",
"notificationsEnabled": "Notificaciones de actualización activas — clic para silenciar", "notificationsEnabled": "Notificaciones de actualización activas — clic para silenciar",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "Système d'exploitation + application", "targetBoth": "Système d'exploitation + application",
"lastRun": "Dernière exécution : {date}", "lastRun": "Dernière exécution : {date}",
"runSuccess": "✓ succès", "runSuccess": "✓ succès",
"runPartial": "partiellement terminée",
"runFailed": "✗ échoué", "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 dapplication",
"releaseDelayNone": "Aucun délai dattente",
"releaseDelayDays": "Attendre {days} jour(s)",
"releaseDelayHelp": "Sapplique 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é", "notScheduled": "Non programmé",
"frequency": "Fréquence", "frequency": "Fréquence",
"cronExpression": "Expression Cron", "cronExpression": "Expression Cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Packages de système d'exploitation uniquement", "targetOptionOs": "Packages de système d'exploitation uniquement",
"targetOptionApp": "Candidature uniquement", "targetOptionApp": "Candidature uniquement",
"targetOptionBoth": "Système d'exploitation + application", "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", "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." "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}", "osPlusApp": "Mises à jour du système d'exploitation + {appName}",
"osPlusApps": "Mises à jour du système d'exploitation et des applications", "osPlusApps": "Mises à jour du système d'exploitation et des applications",
"noUpdateMethodTitle": "Aucune méthode de mise à jour disponible", "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 na été identifiée pour cette application. Ajoutez une commande de mise à jour personnalisée.",
"ociImmutableTitle": "Conteneur d'images OCI", "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.", "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", "hideNoticeButton": "Masquer cet avis pour cette application",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Application", "applicationDefaultName": "Application",
"installedLabel": "installé", "installedLabel": "installé",
"upToDateAtLabel": "À jour à", "upToDateAtLabel": "À jour à",
"versionLabel": "version {version}",
"versionTrackingPending": "Suivi des versions en attente : la prochaine vérification programmée indiquera les versions installée et disponible.", "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", "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.", "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}", "hideNoticeAction": "Masquer cet avis pour {appName}",
"detectedByPrefix": "Détecté par", "detectedByPrefix": "Détecté par",
"editApp": "Modifier", "editApp": "Modifier",
"configureUpdater": "Configurer",
"alsoDetectedTitle": "Également détecté dans ce conteneur :", "alsoDetectedTitle": "Également détecté dans ce conteneur :",
"detectedInline": "· détecté par {method}", "detectedInline": "· détecté par {method}",
"customCommandLabel": "Commande de mise à jour personnalisée", "customCommandLabel": "Commande de mise à jour personnalisée",
@@ -1245,9 +1260,9 @@
"removeButton": "Retirer", "removeButton": "Retirer",
"cancelButton": "Annuler", "cancelButton": "Annuler",
"saveButton": "Sauvegarder", "saveButton": "Sauvegarder",
"editCommandButton": "Modifier la commande",
"wireUpCommandButton": "Ajouter une commande de mise à jour personnalisée",
"versionTrackingPendingShort": "Suivi de version en attente.", "versionTrackingPendingShort": "Suivi de version en attente.",
"versionTrackingNotConfigured": "Le suivi des versions nest pas configuré.",
"managedByOsPackages": "Cette application est mise à jour par laction 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.", "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 longlet Application ou ignorez cet avis.", "noMethodHideBody": "Cette application n'a aucune méthode de mise à jour enregistrée. Définissez-en un dans longlet Application ou ignorez cet avis.",
"hideForApp": "Masquer l'avis pour {appName}", "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.", "noManagedUpdateInfo": "Aucune information de mise à jour pour l'instant - vérifiez depuis Sécurité → Secure Gateway.",
"ociTitle": "Conteneur d'images OCI", "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.", "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.", "dockerImagesTitle": "Images Docker",
"helperDetectedTitle": "Détection d'un programme de mise à jour des scripts d'assistance", "dockerAppTitle": "Docker",
"helperDetectedBody": "Lapplication manuelle est la plus sûre.", "dockerImagesSubheading": "Images",
"dockerImagesReadOnly": "Les tags sont comparés par digest de registre. Les services Compose peuvent télécharger limage et être recréés depuis leur projet déclaré.",
"dockerEngineManagedByOs": "Docker Engine {version} est géré par paquets et mis à jour par laction 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 dAdGuard Home se met à jour depuis sa propre interface web.",
"openAdguard": "Ouvrir AdGuard Home",
"updateDockerImage": "Mettre à jour limage",
"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 na été trouvée.",
"dockerInventoryStarting": "En attente de la fin du démarrage de Docker…",
"dockerInventoryUnavailable": "Linventaire Docker na 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 dimage Docker détectée(s). Utilisez le flux Docker ou Compose correspondant.",
"postApplyChecking": "Vérification du résultat de la mise à jour…", "postApplyChecking": "Vérification du résultat de la mise à jour…",
"postApplyAllOk": "{count} package(s) appliqué(s) avec succès  rien en attente.", "postApplyAllOk": "{count} package(s) appliqué(s) avec succès  rien en attente.",
"postApplyNothingPending": "Rien en attente tout est à jour.", "postApplyNothingPending": "Rien en attente tout est à jour.",
"postApplyPartial": "{pending} package(s) toujours en attente après l'exécution.", "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." "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 nest disponible.",
"staleTarget": "Cette sélection nest plus disponible. Retirez-la avant denregistrer.",
"dockerInventoryPending": "En attente de linventaire 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 denregistrer 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": { "appEditor": {
"closePanel": "Fermer le panneau", "closePanel": "Fermer le panneau",
"cancelButton": "Annuler", "cancelButton": "Annuler",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Actuellement masqué — réapparaîtra dans la liste de détection", "hiddenBadge": "Actuellement masqué — réapparaîtra dans la liste de détection",
"registerDifferent": "Enregistrez une autre application", "registerDifferent": "Enregistrez une autre application",
"detectedInContainer": "Détecté sur ce conteneur", "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", "installedManaged": "Installé et géré par ProxMenux",
"nameLabel": "Nom", "nameLabel": "Nom",
"installedViaLabel": "Installé via", "installedViaLabel": "Installé via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "par exemple, data.version ou releases[0].tag_name", "jsonPathPlaceholder": "par exemple, data.version ou releases[0].tag_name",
"dockerImageLabel": "Image DockerHub", "dockerImageLabel": "Image DockerHub",
"dockerImagePlaceholder": "par exemple, linuxserver/plex ou nginx", "dockerImagePlaceholder": "par exemple, linuxserver/plex ou nginx",
"dockerVersionedTagsHelp": "À utiliser uniquement pour les tags versionnés. Pour latest, stable ou lts, utilisez les mises à jour dimages 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 dafficher 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 dimages Docker.",
"tagRegexLabel": "Tag regex (avec groupe de capture)", "tagRegexLabel": "Tag regex (avec groupe de capture)",
"tagRegexPlaceholder": "par exemple, v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "par exemple, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Enregistrez une application personnalisée", "registerCustom": "Enregistrez une application personnalisée",
"noAppsTitle": "Aucune candidature enregistré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.", "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 lapplication",
"searchApplications": "Rechercher des applications",
"searchingApplications": "Recherche dapplications…",
"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é", "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.", "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", "saveFailed": "Échec de l'enregistrement",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana a besoin", "binaryArgsHintGrafana": "Grafana a besoin",
"loadFailed": "Impossible de charger la configuration de l'application", "loadFailed": "Impossible de charger la configuration de l'application",
"checkFailed": "La vérification a échoué", "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", "deleteFailed": "Échec de la suppression",
"dismissFailed": "Impossible d'ignorer la détection", "dismissFailed": "Impossible d'ignorer la détection",
"restoreFailed": "Impossible de restaurer la détection", "restoreFailed": "Impossible de restaurer la détection",
@@ -1399,6 +1494,10 @@
"webLinks": "Liens Internet", "webLinks": "Liens Internet",
"addPort": "Ajouter un port", "addPort": "Ajouter un port",
"detectedPorts": "Ports détectés dans le conteneur — cliquez pour ajouter :", "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 nenregistre 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.", "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)", "trackUpstream": "Suivre la version disponible (facultatif)",
"trackOff": "Désactivé : lien uniquement", "trackOff": "Désactivé : lien uniquement",
@@ -1416,7 +1515,7 @@
"checkButton": "Vérifier", "checkButton": "Vérifier",
"editFieldsButton": "Modifier les champs", "editFieldsButton": "Modifier les champs",
"alsoDetectedContainer": "Également détecté sur ce conteneur", "alsoDetectedContainer": "Également détecté sur ce conteneur",
"addAnotherApplication": "Ajouter une autre application", "addAnotherApplication": "Enregistrer une autre application",
"doneButton": "Fait", "doneButton": "Fait",
"editButton": "Modifier", "editButton": "Modifier",
"upstreamErrorTimeout": "expiration du délai d'attente du réseau lors du contact en amont", "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", "targetBoth": "Sistema operativo + applicazione",
"lastRun": "Ultima esecuzione: {date}", "lastRun": "Ultima esecuzione: {date}",
"runSuccess": "✓ successo", "runSuccess": "✓ successo",
"runPartial": "completato parzialmente",
"runFailed": "✗ fallito", "runFailed": "✗ fallito",
"runDeferred": "rinviato per sicurezza",
"runSkipped": "nulla in sospeso",
"releaseDelaySummary": "attesa di {days} giorno/i",
"releaseDelayLabel": "Attendi dopo una nuova versione dellapp",
"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 laggiornamento a ogni pianificazione.",
"notScheduled": "Non programmato", "notScheduled": "Non programmato",
"frequency": "Frequenza", "frequency": "Frequenza",
"cronExpression": "Espressione cron", "cronExpression": "Espressione cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Solo pacchetti del sistema operativo", "targetOptionOs": "Solo pacchetti del sistema operativo",
"targetOptionApp": "Solo applicazione", "targetOptionApp": "Solo applicazione",
"targetOptionBoth": "Sistema operativo + 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", "deleteButton": "Elimina pianificazione",
"deleteConfirm": "Rimuovere gli aggiornamenti pianificati per questo contenitore? Le impostazioni predefinite di applicazione (backup + riavvio) vengono mantenute." "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", "osPlusApp": "Sistema operativo + {appName} aggiornamenti",
"osPlusApps": "Aggiornamenti del sistema operativo e delle app", "osPlusApps": "Aggiornamenti del sistema operativo e delle app",
"noUpdateMethodTitle": "Nessun metodo di aggiornamento disponibile", "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", "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.", "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", "hideNoticeButton": "Nascondi questo avviso per questa app",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Applicazione", "applicationDefaultName": "Applicazione",
"installedLabel": "installato", "installedLabel": "installato",
"upToDateAtLabel": "Aggiornato a", "upToDateAtLabel": "Aggiornato a",
"versionLabel": "versione {version}",
"versionTrackingPending": "Monitoraggio versioni in attesa: il prossimo controllo pianificato mostrerà la versione installata e quella disponibile.", "versionTrackingPending": "Monitoraggio versioni in attesa: il prossimo controllo pianificato mostrerà la versione installata e quella disponibile.",
"customCommandTitle": "Comando di aggiornamento personalizzato", "customCommandTitle": "Comando di aggiornamento personalizzato",
"customCommandBody": "Esegui un comando shell definito dall'utente per aggiornare questa app all'interno del contenitore.", "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}", "hideNoticeAction": "Nascondi questo avviso per {appName}",
"detectedByPrefix": "Rilevato da", "detectedByPrefix": "Rilevato da",
"editApp": "Modificare", "editApp": "Modificare",
"configureUpdater": "Configura",
"alsoDetectedTitle": "Rilevato anche in questo contenitore:", "alsoDetectedTitle": "Rilevato anche in questo contenitore:",
"detectedInline": "· rilevato da {method}", "detectedInline": "· rilevato da {method}",
"customCommandLabel": "Comando di aggiornamento personalizzato", "customCommandLabel": "Comando di aggiornamento personalizzato",
@@ -1245,9 +1260,9 @@
"removeButton": "Rimuovere", "removeButton": "Rimuovere",
"cancelButton": "Cancellare", "cancelButton": "Cancellare",
"saveButton": "Salva", "saveButton": "Salva",
"editCommandButton": "Modifica comando",
"wireUpCommandButton": "Aggiungi comando di aggiornamento personalizzato",
"versionTrackingPendingShort": "Monitoraggio della versione in sospeso.", "versionTrackingPendingShort": "Monitoraggio della versione in sospeso.",
"versionTrackingNotConfigured": "Il monitoraggio della versione non è configurato.",
"managedByOsPackages": "Questa applicazione viene aggiornata tramite lazione 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.", "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.", "noMethodHideBody": "Per questa app non è registrato alcun metodo di aggiornamento. Impostane uno dalla scheda App o ignora questo avviso.",
"hideForApp": "Nascondi avviso per {appName}", "hideForApp": "Nascondi avviso per {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Nessuna informazione di aggiornamento ancora: controlla da Sicurezza → Secure Gateway.", "noManagedUpdateInfo": "Nessuna informazione di aggiornamento ancora: controlla da Sicurezza → Secure Gateway.",
"ociTitle": "Contenitore di immagini OCI", "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.", "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.", "dockerImagesTitle": "Immagini Docker",
"helperDetectedTitle": "Rilevato un aggiornamento degli script helper", "dockerAppTitle": "Docker",
"helperDetectedBody": "L'applicazione manuale è più sicura.", "dockerImagesSubheading": "Immagini",
"dockerImagesReadOnly": "I tag vengono confrontati tramite digest del registro. I servizi Compose possono scaricare limmagine ed essere ricreati dal progetto dichiarato.",
"dockerEngineManagedByOs": "Docker Engine {version} è gestito dai pacchetti e viene aggiornato dallazione 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 lavvio…",
"dockerInventoryUnavailable": "Non è stato possibile leggere linventario 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…", "postApplyChecking": "Verifica del risultato dell'aggiornamento…",
"postApplyAllOk": "pacchetto/i {count} applicato correttamente: nulla in sospeso.", "postApplyAllOk": "pacchetto/i {count} applicato correttamente: nulla in sospeso.",
"postApplyNothingPending": "nulla in sospeso: tutto è aggiornato.", "postApplyNothingPending": "nulla in sospeso: tutto è aggiornato.",
"postApplyPartial": "{pending} pacchetto/i ancora in sospeso dopo l'esecuzione.", "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." "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 dellinventario 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 laggiornamento in blocco. Controlla i metodi non disponibili."
},
"appEditor": { "appEditor": {
"closePanel": "Chiudi pannello", "closePanel": "Chiudi pannello",
"cancelButton": "Cancellare", "cancelButton": "Cancellare",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Attualmente nascosto: riapparirà nell'elenco di rilevamento", "hiddenBadge": "Attualmente nascosto: riapparirà nell'elenco di rilevamento",
"registerDifferent": "Registra un'altra app", "registerDifferent": "Registra un'altra app",
"detectedInContainer": "Rilevato su questo contenitore", "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", "installedManaged": "Installato e gestito da ProxMenux",
"nameLabel": "Nome", "nameLabel": "Nome",
"installedViaLabel": "Installato tramite", "installedViaLabel": "Installato tramite",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "ad esempio, data.version o releases[0].tag_name", "jsonPathPlaceholder": "ad esempio, data.version o releases[0].tag_name",
"dockerImageLabel": "Immagine dell'hub Docker", "dockerImageLabel": "Immagine dell'hub Docker",
"dockerImagePlaceholder": "ad esempio, linuxserver/plex o nginx", "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)", "tagRegexLabel": "Tag regex (con gruppo di acquisizione)",
"tagRegexPlaceholder": "ad esempio, v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "ad esempio, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registra un'app personalizzata", "registerCustom": "Registra un'app personalizzata",
"noAppsTitle": "Nessuna domanda registrata", "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.", "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", "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.", "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", "saveFailed": "Salvataggio non riuscito",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana ha bisogno", "binaryArgsHintGrafana": "Grafana ha bisogno",
"loadFailed": "Impossibile caricare la configurazione dell'app", "loadFailed": "Impossibile caricare la configurazione dell'app",
"checkFailed": "Controllo fallito", "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", "deleteFailed": "Eliminazione non riuscita",
"dismissFailed": "Impossibile ignorare il rilevamento", "dismissFailed": "Impossibile ignorare il rilevamento",
"restoreFailed": "Impossibile ripristinare il rilevamento", "restoreFailed": "Impossibile ripristinare il rilevamento",
@@ -1399,6 +1494,10 @@
"webLinks": "Collegamenti Web", "webLinks": "Collegamenti Web",
"addPort": "Aggiungi porto", "addPort": "Aggiungi porto",
"detectedPorts": "Porte rilevate nel contenitore: fai clic per aggiungere:", "detectedPorts": "Porte rilevate nel contenitore: fai clic per aggiungere:",
"dockerPublishedServicesTitle": "Servizi Docker con porte pubblicate",
"dockerPublishedServicesHelp": "Seleziona solo le porte che forniscono uninterfaccia web. ProxMenux non registra questi container come applicazioni LXC indipendenti.",
"dockerPublishedAdd": "Aggiungi link",
"dockerPublishedPort": "porta {containerPort} del container → {hostPort} nellLXC",
"noWebPorts": "Nessuna porta web in ascolto. Utilizza Aggiungi porta se desideri comunque creare un collegamento manualmente.", "noWebPorts": "Nessuna porta web in ascolto. Utilizza Aggiungi porta se desideri comunque creare un collegamento manualmente.",
"trackUpstream": "Monitora la versione disponibile (facoltativo)", "trackUpstream": "Monitora la versione disponibile (facoltativo)",
"trackOff": "Disattivato: solo collegamento", "trackOff": "Disattivato: solo collegamento",
@@ -1416,7 +1515,7 @@
"checkButton": "Controllo", "checkButton": "Controllo",
"editFieldsButton": "Modifica campi", "editFieldsButton": "Modifica campi",
"alsoDetectedContainer": "Rilevato anche su questo contenitore", "alsoDetectedContainer": "Rilevato anche su questo contenitore",
"addAnotherApplication": "Aggiungi un'altra applicazione", "addAnotherApplication": "Registra un'altra applicazione",
"doneButton": "Fatto", "doneButton": "Fatto",
"editButton": "Modificare", "editButton": "Modificare",
"upstreamErrorTimeout": "timeout della rete durante il contatto a monte", "upstreamErrorTimeout": "timeout della rete durante il contatto a monte",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "SO + aplicativo", "targetBoth": "SO + aplicativo",
"lastRun": "Última execução: {date}", "lastRun": "Última execução: {date}",
"runSuccess": "✓ sucesso", "runSuccess": "✓ sucesso",
"runPartial": "concluída parcialmente",
"runFailed": "✗ falhou", "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", "notScheduled": "Não agendado",
"frequency": "Freqüência", "frequency": "Freqüência",
"cronExpression": "Expressão Cron", "cronExpression": "Expressão Cron",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Somente pacotes de sistema operacional", "targetOptionOs": "Somente pacotes de sistema operacional",
"targetOptionApp": "Somente aplicativo", "targetOptionApp": "Somente aplicativo",
"targetOptionBoth": "SO + 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", "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." "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}", "osPlusApp": "SO + atualizações de {appName}",
"osPlusApps": "Atualizações do sistema operacional + aplicativos", "osPlusApps": "Atualizações do sistema operacional + aplicativos",
"noUpdateMethodTitle": "Nenhum método de atualização disponível", "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", "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.", "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", "hideNoticeButton": "Ocultar este aviso para este app",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Aplicativo", "applicationDefaultName": "Aplicativo",
"installedLabel": "instalado", "installedLabel": "instalado",
"upToDateAtLabel": "Atualizado em", "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.", "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", "customCommandTitle": "Comando de atualização personalizado",
"customCommandBody": "Execute um comando shell definido pelo usuário para atualizar este aplicativo dentro do contêiner.", "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}", "hideNoticeAction": "Ocultar este aviso para {appName}",
"detectedByPrefix": "Detectado por", "detectedByPrefix": "Detectado por",
"editApp": "Editar", "editApp": "Editar",
"configureUpdater": "Configurar",
"alsoDetectedTitle": "Também detectado neste contêiner:", "alsoDetectedTitle": "Também detectado neste contêiner:",
"detectedInline": "· detectado por {method}", "detectedInline": "· detectado por {method}",
"customCommandLabel": "Comando de atualização personalizado", "customCommandLabel": "Comando de atualização personalizado",
@@ -1245,9 +1260,9 @@
"removeButton": "Remover", "removeButton": "Remover",
"cancelButton": "Cancelar", "cancelButton": "Cancelar",
"saveButton": "Salvar", "saveButton": "Salvar",
"editCommandButton": "Editar comando",
"wireUpCommandButton": "Adicionar comando de atualização personalizado",
"versionTrackingPendingShort": "Rastreamento de versão pendente.", "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.", "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.", "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}", "hideForApp": "Ocultar aviso para {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Nenhuma informação de atualização ainda — verifique em Segurança → Secure Gateway.", "noManagedUpdateInfo": "Nenhuma informação de atualização ainda — verifique em Segurança → Secure Gateway.",
"ociTitle": "Contêiner de imagem OCI", "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.", "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.", "dockerImagesTitle": "Imagens Docker",
"helperDetectedTitle": "Detectou um atualizador de scripts auxiliares", "dockerAppTitle": "Docker",
"helperDetectedBody": "Aplicar manualmente é mais seguro.", "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…", "postApplyChecking": "Verificando o resultado da atualização…",
"postApplyAllOk": "{count} pacote(s) aplicado(s) com sucesso — nada pendente.", "postApplyAllOk": "{count} pacote(s) aplicado(s) com sucesso — nada pendente.",
"postApplyNothingPending": "Nada pendente — tudo está atualizado.", "postApplyNothingPending": "Nada pendente — tudo está atualizado.",
"postApplyPartial": "{pending} pacote(s) ainda pendente(s) após a execução.", "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." "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": { "appEditor": {
"closePanel": "Fechar painel", "closePanel": "Fechar painel",
"cancelButton": "Cancelar", "cancelButton": "Cancelar",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "Atualmente oculto — reaparecerá na lista de detecção", "hiddenBadge": "Atualmente oculto — reaparecerá na lista de detecção",
"registerDifferent": "Registre um aplicativo diferente", "registerDifferent": "Registre um aplicativo diferente",
"detectedInContainer": "Detectado neste contêiner", "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", "installedManaged": "Instalado e gerenciado por ProxMenux",
"nameLabel": "Nome", "nameLabel": "Nome",
"installedViaLabel": "Instalado via", "installedViaLabel": "Instalado via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "por exemplo, data.version ou releases[0].tag_name", "jsonPathPlaceholder": "por exemplo, data.version ou releases[0].tag_name",
"dockerImageLabel": "Imagem do DockerHub", "dockerImageLabel": "Imagem do DockerHub",
"dockerImagePlaceholder": "por exemplo, linuxserver/plex ou nginx", "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)", "tagRegexLabel": "Tag regex (com grupo de captura)",
"tagRegexPlaceholder": "por exemplo, v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "por exemplo, v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registre um aplicativo personalizado", "registerCustom": "Registre um aplicativo personalizado",
"noAppsTitle": "Nenhum aplicativo cadastrado", "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.", "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", "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.", "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", "saveFailed": "Falha ao salvar",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Necessidades de grafana", "binaryArgsHintGrafana": "Necessidades de grafana",
"loadFailed": "Não foi possível carregar a configuração do aplicativo", "loadFailed": "Não foi possível carregar a configuração do aplicativo",
"checkFailed": "Falha na verificação", "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", "deleteFailed": "Falha na exclusão",
"dismissFailed": "Não foi possível descartar a detecção", "dismissFailed": "Não foi possível descartar a detecção",
"restoreFailed": "Não foi possível restaurar a detecção", "restoreFailed": "Não foi possível restaurar a detecção",
@@ -1399,6 +1494,10 @@
"webLinks": "Links da web", "webLinks": "Links da web",
"addPort": "Adicionar porta", "addPort": "Adicionar porta",
"detectedPorts": "Portas detectadas no contêiner — clique para adicionar:", "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.", "noWebPorts": "Nenhuma porta da web escutando. Use Adicionar porta se ainda quiser criar um link manualmente.",
"trackUpstream": "Rastrear versão disponível (opcional)", "trackUpstream": "Rastrear versão disponível (opcional)",
"trackOff": "Desativado apenas link", "trackOff": "Desativado apenas link",
@@ -1416,7 +1515,7 @@
"checkButton": "Verificar", "checkButton": "Verificar",
"editFieldsButton": "Editar campos", "editFieldsButton": "Editar campos",
"alsoDetectedContainer": "Também detectado neste contêiner", "alsoDetectedContainer": "Também detectado neste contêiner",
"addAnotherApplication": "Adicione outro aplicativo", "addAnotherApplication": "Registar outra aplicação",
"doneButton": "Feito", "doneButton": "Feito",
"editButton": "Editar", "editButton": "Editar",
"upstreamErrorTimeout": "Tempo limite da rede ao entrar em contato com o upstream", "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", "targetBoth": "Systém + aplikácia",
"lastRun": "Posledné spustenie: {date}", "lastRun": "Posledné spustenie: {date}",
"runSuccess": "✓ úspešné", "runSuccess": "✓ úspešné",
"runPartial": "čiastočne dokončené",
"runFailed": "✗ zlyhalo", "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", "notScheduled": "Bez plánu",
"frequency": "Frekvencia", "frequency": "Frekvencia",
"cronExpression": "Cron výraz", "cronExpression": "Cron výraz",
@@ -1171,6 +1179,11 @@
"targetOptionOs": "Iba balíky systému", "targetOptionOs": "Iba balíky systému",
"targetOptionApp": "Iba aplikáciu", "targetOptionApp": "Iba aplikáciu",
"targetOptionBoth": "Systém + 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", "deleteButton": "Odstrániť plán",
"deleteConfirm": "Odstrániť plánované aktualizácie tohto kontajnera? Predvolené nastavenia (záloha a reštart) zostanú zachované." "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}", "osPlusApp": "Aktualizácie systému + {appName}",
"osPlusApps": "Aktualizácie systému + aplikácií", "osPlusApps": "Aktualizácie systému + aplikácií",
"noUpdateMethodTitle": "Aktualizácia nie je nastavená", "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", "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.", "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", "hideNoticeButton": "Skryť toto upozornenie pre aplikáciu",
@@ -1229,6 +1242,7 @@
"applicationDefaultName": "Aplikácia", "applicationDefaultName": "Aplikácia",
"installedLabel": "nainštalované", "installedLabel": "nainštalované",
"upToDateAtLabel": "Aktuálna verzia", "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.", "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", "customCommandTitle": "Vlastný príkaz na aktualizáciu",
"customCommandBody": "Spustí v kontajneri vlastný shell príkaz, ktorý aktualizuje túto aplikáciu.", "customCommandBody": "Spustí v kontajneri vlastný shell príkaz, ktorý aktualizuje túto aplikáciu.",
@@ -1237,6 +1251,7 @@
"hideNoticeAction": "Skryť upozornenie pre {appName}", "hideNoticeAction": "Skryť upozornenie pre {appName}",
"detectedByPrefix": "Zistené cez", "detectedByPrefix": "Zistené cez",
"editApp": "Upraviť", "editApp": "Upraviť",
"configureUpdater": "Nastaviť",
"alsoDetectedTitle": "V tomto kontajneri sa našli aj:", "alsoDetectedTitle": "V tomto kontajneri sa našli aj:",
"detectedInline": "· zistené cez {method}", "detectedInline": "· zistené cez {method}",
"customCommandLabel": "Vlastný príkaz na aktualizáciu", "customCommandLabel": "Vlastný príkaz na aktualizáciu",
@@ -1244,9 +1259,9 @@
"removeButton": "Odstrániť", "removeButton": "Odstrániť",
"cancelButton": "Zrušiť", "cancelButton": "Zrušiť",
"saveButton": "Uložiť", "saveButton": "Uložiť",
"editCommandButton": "Upraviť príkaz",
"wireUpCommandButton": "Pridať vlastný príkaz na aktualizáciu",
"versionTrackingPendingShort": "Čaká sa na údaje o verzii.", "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.", "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.", "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}", "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.", "noManagedUpdateInfo": "Zatiaľ nie sú žiadne informácie o aktualizácii skontrolujte ich v časti Zabezpečenie → Secure Gateway.",
"ociTitle": "Kontajner z OCI obrazu", "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.", "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ú.", "dockerImagesTitle": "Docker obrazy",
"helperDetectedTitle": "Našiel sa aktualizačný skript z helper-scripts", "dockerAppTitle": "Docker",
"helperDetectedBody": "Najbezpečnejšie je spustiť aktualizáciu ručne.", "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…", "postApplyChecking": "Overuje sa výsledok aktualizácie…",
"postApplyAllOk": "{count} balíkov bolo úspešne použitých nič sa nečaká.", "postApplyAllOk": "{count} balíkov bolo úspešne použitých nič sa nečaká.",
"postApplyNothingPending": "Nič sa nečaká všetko je aktuálne.", "postApplyNothingPending": "Nič sa nečaká všetko je aktuálne.",
"postApplyPartial": "{pending} balíkov, ktoré po spustení stále čakajú.", "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." "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": { "appEditor": {
"closePanel": "Zavrieť panel", "closePanel": "Zavrieť panel",
"cancelButton": "Zrušiť", "cancelButton": "Zrušiť",
@@ -1283,6 +1342,17 @@
"hiddenBadge": "Momentálne skryté — po obnovení sa znova zobrazí v zozname nájdených aplikácií", "hiddenBadge": "Momentálne skryté — po obnovení sa znova zobrazí v zozname nájdených aplikácií",
"registerDifferent": "Pridať inú aplikáciu", "registerDifferent": "Pridať inú aplikáciu",
"detectedInContainer": "Nájdené v tomto kontajneri", "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", "installedManaged": "Nainštalované a spravované cez ProxMenux",
"nameLabel": "Názov", "nameLabel": "Názov",
"installedViaLabel": "Spôsob inštalácie", "installedViaLabel": "Spôsob inštalácie",
@@ -1339,6 +1409,17 @@
"jsonPathPlaceholder": "napr. data.version alebo releases[0].tag_name", "jsonPathPlaceholder": "napr. data.version alebo releases[0].tag_name",
"dockerImageLabel": "Obraz na Docker Hub", "dockerImageLabel": "Obraz na Docker Hub",
"dockerImagePlaceholder": "napr. linuxserver/plex alebo nginx", "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)", "tagRegexLabel": "Regex tagu (so zachytávacou skupinou)",
"tagRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1371,7 +1452,14 @@
"registerCustom": "Pridať vlastnú aplikáciu", "registerCustom": "Pridať vlastnú aplikáciu",
"noAppsTitle": "Nie sú pridané žiadne aplikácie", "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.", "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}", "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ť.", "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", "saveFailed": "Uloženie zlyhalo",
@@ -1382,6 +1470,13 @@
"binaryArgsHintGrafana": "Grafana potrebuje", "binaryArgsHintGrafana": "Grafana potrebuje",
"loadFailed": "Nastavenia aplikácií sa nepodarilo načítať", "loadFailed": "Nastavenia aplikácií sa nepodarilo načítať",
"checkFailed": "Kontrola zlyhala", "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", "deleteFailed": "Odstránenie zlyhalo",
"dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť", "dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť",
"restoreFailed": "Nájdenú aplikáciu sa nepodarilo obnoviť", "restoreFailed": "Nájdenú aplikáciu sa nepodarilo obnoviť",
@@ -1398,6 +1493,10 @@
"webLinks": "Webové odkazy", "webLinks": "Webové odkazy",
"addPort": "Pridať port", "addPort": "Pridať port",
"detectedPorts": "Porty nájdené v kontajneri — kliknutím ich pridáte:", "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.", "noWebPorts": "Nenašli sa žiadne aktívne webové porty. Odkaz môžete vytvoriť ručne cez Pridať port.",
"trackUpstream": "Sledovať dostupnú verziu (voliteľné)", "trackUpstream": "Sledovať dostupnú verziu (voliteľné)",
"trackOff": "Vypnuté — iba odkaz", "trackOff": "Vypnuté — iba odkaz",
@@ -1415,7 +1514,7 @@
"checkButton": "Skontrolovať", "checkButton": "Skontrolovať",
"editFieldsButton": "Upraviť údaje", "editFieldsButton": "Upraviť údaje",
"alsoDetectedContainer": "Ďalšie aplikácie nájdené v kontajneri", "alsoDetectedContainer": "Ďalšie aplikácie nájdené v kontajneri",
"addAnotherApplication": "Pridať ďalšiu aplikáciu", "addAnotherApplication": "Registrovať ďalšiu aplikáciu",
"doneButton": "Hotovo", "doneButton": "Hotovo",
"editButton": "Upraviť", "editButton": "Upraviť",
"upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream", "upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
+107 -8
View File
@@ -1163,7 +1163,15 @@
"targetBoth": "OS + applikation", "targetBoth": "OS + applikation",
"lastRun": "Senaste körningen: {date}", "lastRun": "Senaste körningen: {date}",
"runSuccess": "✓ framgång", "runSuccess": "✓ framgång",
"runPartial": "delvis slutförd",
"runFailed": "✗ misslyckades", "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", "notScheduled": "Inte schemalagt",
"frequency": "Frekvens", "frequency": "Frekvens",
"cronExpression": "Cron uttryck", "cronExpression": "Cron uttryck",
@@ -1172,6 +1180,11 @@
"targetOptionOs": "Endast OS-paket", "targetOptionOs": "Endast OS-paket",
"targetOptionApp": "Endast ansökan", "targetOptionApp": "Endast ansökan",
"targetOptionBoth": "OS + applikation", "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", "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." "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", "osPlusApp": "OS + {appName} uppdateringar",
"osPlusApps": "OS + Apps-uppdateringar", "osPlusApps": "OS + Apps-uppdateringar",
"noUpdateMethodTitle": "Ingen uppdateringsmetod tillgänglig", "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", "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.", "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", "hideNoticeButton": "Dölj det här meddelandet för den här appen",
@@ -1230,6 +1243,7 @@
"applicationDefaultName": "Tillämpningen", "applicationDefaultName": "Tillämpningen",
"installedLabel": "installerat", "installedLabel": "installerat",
"upToDateAtLabel": "Uppdaterad kl", "upToDateAtLabel": "Uppdaterad kl",
"versionLabel": "version {version}",
"versionTrackingPending": "Versionsspårning väntar nästa schemalagda kontroll visar installerad och tillgänglig version.", "versionTrackingPending": "Versionsspårning väntar nästa schemalagda kontroll visar installerad och tillgänglig version.",
"customCommandTitle": "Anpassat uppdateringskommando", "customCommandTitle": "Anpassat uppdateringskommando",
"customCommandBody": "Kör ett användardefinierat skalkommando för att uppdatera den här appen inuti behållaren.", "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}", "hideNoticeAction": "Dölj detta meddelande för {appName}",
"detectedByPrefix": "Upptäckt av", "detectedByPrefix": "Upptäckt av",
"editApp": "Redigera", "editApp": "Redigera",
"configureUpdater": "Konfigurera",
"alsoDetectedTitle": "Detekteras även i den här behållaren:", "alsoDetectedTitle": "Detekteras även i den här behållaren:",
"detectedInline": "· upptäckt av {method}", "detectedInline": "· upptäckt av {method}",
"customCommandLabel": "Anpassat uppdateringskommando", "customCommandLabel": "Anpassat uppdateringskommando",
@@ -1245,9 +1260,9 @@
"removeButton": "Ta bort", "removeButton": "Ta bort",
"cancelButton": "Avbryt", "cancelButton": "Avbryt",
"saveButton": "Spara", "saveButton": "Spara",
"editCommandButton": "Redigera kommando",
"wireUpCommandButton": "Lägg till anpassat uppdateringskommando",
"versionTrackingPendingShort": "Väntande versionsspårning.", "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.", "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.", "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}", "hideForApp": "Dölj meddelande för {appName}",
@@ -1266,15 +1281,59 @@
"noManagedUpdateInfo": "Ingen uppdateringsinformation ännu — kolla från Säkerhet → Secure Gateway.", "noManagedUpdateInfo": "Ingen uppdateringsinformation ännu — kolla från Säkerhet → Secure Gateway.",
"ociTitle": "OCI-bildbehållare", "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.", "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.", "dockerImagesTitle": "Docker-avbilder",
"helperDetectedTitle": "Upptäckte en hjälparskriptuppdatering", "dockerAppTitle": "Docker",
"helperDetectedBody": "Att applicera manuellt är säkrast.", "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...", "postApplyChecking": "Verifierar uppdateringsresultat...",
"postApplyAllOk": "{count} paket(en) har tillämpats framgångsrikt — inget väntande.", "postApplyAllOk": "{count} paket(en) har tillämpats framgångsrikt — inget väntande.",
"postApplyNothingPending": "Inget väntande — allt är uppdaterat.", "postApplyNothingPending": "Inget väntande — allt är uppdaterat.",
"postApplyPartial": "{pending} paket som fortfarande väntar efter körningen.", "postApplyPartial": "{pending} paket som fortfarande väntar efter körningen.",
"postApplyPartialSubline": "{applied} tillämpas.Vissa uppdateringar slutfördes inte granska terminalutgången ovan." "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": { "appEditor": {
"closePanel": "Stäng panelen", "closePanel": "Stäng panelen",
"cancelButton": "Avbryt", "cancelButton": "Avbryt",
@@ -1284,6 +1343,17 @@
"hiddenBadge": "För närvarande dold — kommer att dyka upp igen i upptäcktslistan", "hiddenBadge": "För närvarande dold — kommer att dyka upp igen i upptäcktslistan",
"registerDifferent": "Registrera en annan app", "registerDifferent": "Registrera en annan app",
"detectedInContainer": "Upptäcks på den här behållaren", "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", "installedManaged": "Installerad och hanterad av ProxMenux",
"nameLabel": "Namn", "nameLabel": "Namn",
"installedViaLabel": "Installerad via", "installedViaLabel": "Installerad via",
@@ -1340,6 +1410,17 @@
"jsonPathPlaceholder": "t.ex. data.version eller releases[0].tag_name", "jsonPathPlaceholder": "t.ex. data.version eller releases[0].tag_name",
"dockerImageLabel": "Docker Hub-bild", "dockerImageLabel": "Docker Hub-bild",
"dockerImagePlaceholder": "t.ex. linuxserver/plex eller nginx", "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)", "tagRegexLabel": "Taggregex (med fångstgrupp)",
"tagRegexPlaceholder": "t.ex. v?(\\d+\\.\\d+\\.\\d+)", "tagRegexPlaceholder": "t.ex. v?(\\d+\\.\\d+\\.\\d+)",
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)", "tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
@@ -1372,7 +1453,14 @@
"registerCustom": "Registrera en anpassad app", "registerCustom": "Registrera en anpassad app",
"noAppsTitle": "Inga ansökningar registrerade", "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.", "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", "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.", "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", "saveFailed": "Det gick inte att spara",
@@ -1383,6 +1471,13 @@
"binaryArgsHintGrafana": "Grafana behöver", "binaryArgsHintGrafana": "Grafana behöver",
"loadFailed": "Det gick inte att läsa in appkonfigurationen", "loadFailed": "Det gick inte att läsa in appkonfigurationen",
"checkFailed": "Kontrollen misslyckades", "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", "deleteFailed": "Det gick inte att ta bort",
"dismissFailed": "Det gick inte att avvisa upptäckten", "dismissFailed": "Det gick inte att avvisa upptäckten",
"restoreFailed": "Det gick inte att återställa upptäckten", "restoreFailed": "Det gick inte att återställa upptäckten",
@@ -1399,6 +1494,10 @@
"webLinks": "Webblänkar", "webLinks": "Webblänkar",
"addPort": "Lägg till port", "addPort": "Lägg till port",
"detectedPorts": "Portar upptäckta i behållaren — klicka för att lägga till:", "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.", "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)", "trackUpstream": "Spåra tillgänglig version (valfritt)",
"trackOff": "Av endast länk", "trackOff": "Av endast länk",
@@ -1416,7 +1515,7 @@
"checkButton": "Kontrollera", "checkButton": "Kontrollera",
"editFieldsButton": "Redigera fält", "editFieldsButton": "Redigera fält",
"alsoDetectedContainer": "Detekteras även på denna behållare", "alsoDetectedContainer": "Detekteras även på denna behållare",
"addAnotherApplication": "Lägg till ytterligare ett program", "addAnotherApplication": "Registrera en annan app",
"doneButton": "Gjort", "doneButton": "Gjort",
"editButton": "Redigera", "editButton": "Redigera",
"upstreamErrorTimeout": "Nätverkstimeout vid kontakt uppströms", "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/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/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/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 "$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/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" 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 terminal sessions
active_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']) @terminal_bp.route('/api/terminal/health', methods=['GET'])
def terminal_health(): def terminal_health():
"""Health check for terminal service""" """Health check for terminal service"""
@@ -470,6 +496,7 @@ def script_websocket(ws, session_id):
env['PYTHONUNBUFFERED'] = '1' env['PYTHONUNBUFFERED'] = '1'
env['TERM'] = 'xterm-256color' env['TERM'] = 'xterm-256color'
script_started_at = time.monotonic()
script_process = subprocess.Popen( script_process = subprocess.Popen(
['/bin/bash', script_path], ['/bin/bash', script_path],
stdin=slave_fd, stdin=slave_fd,
@@ -580,6 +607,18 @@ def script_websocket(ws, session_id):
script_process.wait() script_process.wait()
exit_code = script_process.returncode if script_process.returncode is not None else 0 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: try:
ws.send(f'\r\n[Script exited with code {exit_code}]\r\n') ws.send(f'\r\n[Script exited with code {exit_code}]\r\n')
# Send an explicit terminal result before the connection is # Send an explicit terminal result before the connection is
File diff suppressed because it is too large Load Diff
+222 -87
View File
@@ -167,6 +167,26 @@ def _detect_nvidia_xfree86() -> Optional[dict]:
# libedgetpu1-std from Google's apt repo). # 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]: def _detect_coral_host() -> list[dict]:
out: list[dict] = [] out: list[dict] = []
@@ -180,37 +200,70 @@ def _detect_coral_host() -> list[dict]:
# knows the fork's patch level. # knows the fork's patch level.
# 2. `dpkg-query gasket-dkms` — the Debian package version, only # 2. `dpkg-query gasket-dkms` — the Debian package version, only
# present when the user installed via .deb rather than the # 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 # 3. `dkms status` — the upstream module version registered with
# DKMS, which is always the bare `1.0`. Useful as a "modules # DKMS, which is always the bare `1.0`. Useful as a "modules
# are present" indicator but doesn't reveal the fork patch # are present" indicator but doesn't reveal the fork patch
# level, so the update-availability check would always fire a # level, so the update-availability check would always fire a
# false positive against feranick's `1.0-N` tags. Reported on # false positive against feranick's `1.0-N` tags.
# .50 after a successful re-install kept showing the update #
# notification. # Orphan detection: gasket-dkms package present + no PCIe/M.2
pcie_version: Optional[str] = None # 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: try:
with open("/var/lib/proxmenux/coral_gasket_version", with open("/var/lib/proxmenux/coral_gasket_version",
"r", encoding="utf-8", errors="replace") as fh: "r", encoding="utf-8", errors="replace") as fh:
marker = fh.read().strip() 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): if marker and re.match(r"^[A-Za-z0-9._+-]+$", marker):
pcie_version = marker marker_version = marker
except OSError: except OSError:
pass pass
if not pcie_version: # gasket-dkms package inspection: state + version, kept separate.
dpkg_state: str = "absent" # "healthy" | "broken" | "absent"
dpkg_version: Optional[str] = None
try: try:
r = subprocess.run( r = subprocess.run(
["dpkg-query", "-W", "-f=${Status}|${Version}", "gasket-dkms"], ["dpkg-query", "-W", "-f=${Status}|${Version}", "gasket-dkms"],
capture_output=True, text=True, timeout=3, capture_output=True, text=True, timeout=3,
) )
if r.returncode == 0 and "ok installed" in r.stdout: if r.returncode == 0 and "|" in r.stdout:
pcie_version = r.stdout.split("|", 1)[1].strip() 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): except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass pass
if not pcie_version:
# 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: try:
r = subprocess.run( r = subprocess.run(
["dkms", "status"], capture_output=True, text=True, timeout=3, ["dkms", "status"], capture_output=True, text=True, timeout=3,
@@ -218,23 +271,34 @@ def _detect_coral_host() -> list[dict]:
if r.returncode == 0: if r.returncode == 0:
for line in r.stdout.splitlines(): for line in r.stdout.splitlines():
if line.startswith("gasket"): if line.startswith("gasket"):
# "gasket, 1.0, ..." or "gasket/1.0, ..."
m = re.match(r"^gasket[, /]([^,\s]+)", line) m = re.match(r"^gasket[, /]([^,\s]+)", line)
if m: if m:
pcie_version = m.group(1) pcie_version = m.group(1)
break break
except (FileNotFoundError, OSError, subprocess.TimeoutExpired): except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
pass pass
if pcie_version:
out.append({ 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", "id": "coral-host-pcie",
"type": "coral_host", "type": "coral_host",
"name": "Coral TPU Driver (gasket-dkms)", "name": "Coral TPU Driver (gasket-dkms)",
"current_version": pcie_version,
"menu_label": "GPU & TPU → Coral TPU", "menu_label": "GPU & TPU → Coral TPU",
"menu_script": "scripts/gpu_tpu/install_coral.sh", "menu_script": "scripts/gpu_tpu/install_coral.sh",
"_coral_variant": "pcie", "_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 # USB — libedgetpu1-std (default) or libedgetpu1-max if the user
# opted into the overclocked runtime. Either one means the USB # 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: Optional[dict] = None
_helpers_cache_ts: float = 0.0 _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: def _fetch_helpers_cache() -> dict:
@@ -714,22 +782,30 @@ def _guess_helper_slug_from_hostname(hostname: str) -> Optional[str]:
return None return None
def _infer_helper_slug(vmid: str, hostname: str) -> Optional[str]: def _identify_helper_slug(vmid: str, hostname: str) -> tuple[Optional[str], Optional[str]]:
"""Best-effort identification of the community-scripts slug for a CT. """Return ``(slug, evidence_source)`` for a community-scripts CT.
Primary: extract from /usr/bin/update (present on installs from a ``update_wrapper`` is executable evidence: the slug was extracted
reasonably modern community-scripts installer). Fallback: if the from /usr/bin/update. ``tag_hostname`` is only an identity hint for
CT carries a helper-scripts tag but /usr/bin/update is missing old installs and must never enable an update action by itself.
(very old installs, or the file was removed), guess by
fuzzy-matching the hostname against the helpers_cache slug list.
""" """
slug = _probe_helper_scripts_slug(vmid) slug = _probe_helper_scripts_slug(vmid)
if slug: if slug:
return slug return slug, "update_wrapper"
tags = _probe_lxc_tags(vmid) tags = _probe_lxc_tags(vmid)
if not (tags & _HELPER_SCRIPTS_TAGS): if not (tags & _HELPER_SCRIPTS_TAGS):
return None return None, None
return _guess_helper_slug_from_hostname(hostname) 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]: def _probe_lxc_os(vmid: str) -> Optional[str]:
@@ -766,7 +842,7 @@ def _probe_lxc_os(vmid: str) -> Optional[str]:
return None 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. """Enumerate running Debian/Ubuntu CTs as registry entries.
OS detection is cached in the registry entry (`_os_family`), so the 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] = [] out: list[dict] = []
for ct in cts: for ct in cts:
if only_vmid is not None and str(ct.get("vmid")) != str(int(only_vmid)):
continue
if ct["status"] != "running": if ct["status"] != "running":
continue continue
vmid = ct["vmid"] vmid = ct["vmid"]
@@ -853,16 +931,23 @@ def _detect_lxc_containers() -> list[dict]:
# Jellyfin" rather than a generic "Update"). # Jellyfin" rather than a generic "Update").
has_app_updater = False has_app_updater = False
helper_slug: Optional[str] = None helper_slug: Optional[str] = None
helper_slug_source: Optional[str] = None
helper_app_name: Optional[str] = None helper_app_name: Optional[str] = None
helper_updateable_known = False # True when we found the slug in the cache helper_updateable_known = False # True when we found the slug in the cache
if not is_oci and not managed_oci_app: 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: if helper_slug:
entry = _fetch_helpers_cache().get(helper_slug) entry = _fetch_helpers_cache().get(helper_slug)
if entry: if entry:
helper_updateable_known = True helper_updateable_known = True
helper_app_name = entry.get("name") or helper_slug 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({ out.append({
"id": cid, "id": cid,
@@ -877,6 +962,7 @@ def _detect_lxc_containers() -> list[dict]:
"_managed_oci_app": managed_oci_app, "_managed_oci_app": managed_oci_app,
"_has_app_updater": has_app_updater, "_has_app_updater": has_app_updater,
"_helper_slug": helper_slug, "_helper_slug": helper_slug,
"_helper_slug_source": helper_slug_source,
"_helper_app_name": helper_app_name, "_helper_app_name": helper_app_name,
"_helper_updateable_known": helper_updateable_known, "_helper_updateable_known": helper_updateable_known,
}) })
@@ -904,6 +990,45 @@ def _normalise_detector_result(result: Any) -> list[dict]:
return [] 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: def detect_and_register() -> dict:
"""Run every detector, merge results into the registry, persist. """Run every detector, merge results into the registry, persist.
@@ -938,44 +1063,9 @@ def detect_and_register() -> dict:
# 1. Add new + reactivate / refresh existing. # 1. Add new + reactivate / refresh existing.
for item_id, entry in discovered.items(): for item_id, entry in discovered.items():
if item_id in index: if item_id in index:
existing = items[index[item_id]] _merge_detected_entry(items[index[item_id]], entry, now)
# 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
else: else:
# Brand new entry items.append(_new_detected_entry(entry, now))
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)
# 2. Mark missing items as removed (don't delete — preserve # 2. Mark missing items as removed (don't delete — preserve
# history so a reinstall doesn't lose the audit trail). # 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]: def check_for_updates(force: bool = False) -> list[dict]:
"""Run every type-specific checker over active items, persist """Run every type-specific checker over active items, persist
the updated state, return the list of items that have an update 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, result = {"available": False, "latest": None,
"last_check": _now_iso(), "error": str(e)} "last_check": _now_iso(), "error": str(e)}
it["update_check"] = { _store_update_result(it, result)
"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]
if it["update_check"]["available"]: if it["update_check"]["available"]:
updates_available.append(it) updates_available.append(it)
@@ -1650,3 +1739,49 @@ def check_for_updates(force: bool = False) -> list[dict]:
_write_registry(reg) _write_registry(reg)
return updates_available 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 subprocess
import threading import threading
from queue import Queue from queue import Queue
from typing import Optional, Dict, Any, Tuple from typing import Optional, Dict, Any, Tuple, Callable
from pathlib import Path from pathlib import Path
@@ -1939,8 +1939,16 @@ class TaskWatcher:
'vzmigrate': ('migration_start', 'INFO'), '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 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._running = False
self._thread: Optional[threading.Thread] = None self._thread: Optional[threading.Thread] = None
# `_hostname` is exposed as a @property below so every read returns # `_hostname` is exposed as a @property below so every read returns
@@ -2251,6 +2259,31 @@ class TaskWatcher:
# Determine entity type from task type # Determine entity type from task type
entity = 'ct' if task_type.startswith('vz') else 'vm' 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 # Backup completion/failure and replication events are handled
# EXCLUSIVELY by the PVE webhook, which delivers richer data (full # EXCLUSIVELY by the PVE webhook, which delivers richer data (full
# logs, sizes, durations, filenames). TaskWatcher skips these to # logs, sizes, durations, filenames). TaskWatcher skips these to
@@ -3516,6 +3549,15 @@ class PollingCollector:
try: try:
import lxc_apps import lxc_apps
lxc_apps.refresh_all_apps(force=False) 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 # After the refresh, emit `app_update_available` for every
# sidecar entry currently flagged with a pending upstream # sidecar entry currently flagged with a pending upstream
# release. `check_app(force=False)` short-circuits on a # release. `check_app(force=False)` short-circuits on a
@@ -3526,6 +3568,7 @@ class PollingCollector:
# moment. `notification_manager` dedups by entity_id # moment. `notification_manager` dedups by entity_id
# (vmid + app_id + latest_version) so repeated calls only # (vmid + app_id + latest_version) so repeated calls only
# deliver one notification per release. # deliver one notification per release.
lxc_apps.emit_all_pending_docker_stacks()
lxc_apps.emit_all_pending_updates() lxc_apps.emit_all_pending_updates()
except Exception as e: except Exception as e:
print(f"[PollingCollector] lxc_apps refresh failed: {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 # at once, so without this exemption only the first 1-2 land and
# the rest get buffered into a useless summary. # the rest get buffered into a useless summary.
'app_update_available', 'app_update_available',
'docker_stack_update_available',
'lxc_update_applied',
}) })
@@ -791,6 +793,7 @@ class NotificationManager:
self._task_watcher: Optional[TaskWatcher] = None self._task_watcher: Optional[TaskWatcher] = None
self._polling_collector: Optional[PollingCollector] = None self._polling_collector: Optional[PollingCollector] = None
self._dispatch_thread: Optional[threading.Thread] = None self._dispatch_thread: Optional[threading.Thread] = None
self._guest_lifecycle_callback = None
# Webhook receiver (no thread, passive) # Webhook receiver (no thread, passive)
self._hook_watcher: Optional[ProxmoxHookWatcher] = None self._hook_watcher: Optional[ProxmoxHookWatcher] = None
@@ -982,6 +985,17 @@ class NotificationManager:
self._load_config() self._load_config()
return {'success': True, 'channels': list(self._channels.keys())} 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 TaskWatcherthe 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) ────────────────────────────── # ─── Server Mode (Background) ──────────────────────────────
def start(self): def start(self):
@@ -1017,7 +1031,10 @@ class NotificationManager:
# polling collector keep the managed_installs registry, the # polling collector keep the managed_installs registry, the
# error history, and the task state up to date. # error history, and the task state up to date.
self._journal_watcher = JournalWatcher(self._event_queue) 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._polling_collector = PollingCollector(self._event_queue)
self._journal_watcher.start() self._journal_watcher.start()
@@ -1966,6 +1983,7 @@ class NotificationManager:
'coral_driver_update_available', 'coral_driver_update_available',
'secure_gateway_update_available', 'secure_gateway_update_available',
'app_update_available', 'app_update_available',
'docker_stack_update_available',
# Security events that must not be silenced by stale cooldowns # Security events that must not be silenced by stale cooldowns
# following a Monitor reinstall (Pedro Rico, 19/05). # following a Monitor reinstall (Pedro Rico, 19/05).
'auth_fail', 'auth_fail',
+12 -4
View File
@@ -512,10 +512,7 @@ TEMPLATES = {
}, },
'lxc_update_applied': { 'lxc_update_applied': {
'title': '{hostname}: LXC {ct_name} ({vmid}) update {result}', 'title': '{hostname}: LXC {ct_name} ({vmid}) update {result}',
'body': ( 'body': '{details}',
'Container {ct_name} (CT {vmid}) — update {result}.\n'
'Target: {target} Duration: {duration}'
),
'label': 'LXC update applied', 'label': 'LXC update applied',
'group': 'vm_ct', 'group': 'vm_ct',
'default_enabled': True, 'default_enabled': True,
@@ -540,6 +537,16 @@ TEMPLATES = {
# never received the notification they explicitly asked for. # never received the notification they explicitly asked for.
'default_enabled': True, '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': { 'vm_start': {
'title': '{hostname}: VM {vmname} ({vmid}) started', 'title': '{hostname}: VM {vmname} ({vmid}) started',
'body': 'Virtual machine {vmname} (ID: {vmid}) is now running.', '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_updates_available': '\U0001F4E6', # \uD83D\uDCE6 package \u2014 pending CT updates
'lxc_update_applied': '\u2705', # \u2705 check \u2014 update applied 'lxc_update_applied': '\u2705', # \u2705 check \u2014 update applied
'app_update_available': '\U0001F195', # \ud83c\udd95 NEW \u2014 upstream app release 'app_update_available': '\U0001F195', # \ud83c\udd95 NEW \u2014 upstream app release
'docker_stack_update_available': '\U0001F433',
'vm_start': '\u25B6\uFE0F', # play button 'vm_start': '\u25B6\uFE0F', # play button
'vm_start_warning': '\u26A0\uFE0F', # warning sign - started with warnings 'vm_start_warning': '\u26A0\uFE0F', # warning sign - started with warnings
'vm_stop': '\u23F9\uFE0F', # stop button '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())
File diff suppressed because it is too large Load Diff
+138 -4
View File
@@ -1,8 +1,121 @@
{ {
"schema_version": 1, "schema_version": 1,
"observed_at": "2026-08-06", "observed_at": "2026-08-21",
"notes": "Runtime-verified detectors for the LXC app catalog. Every entry is a detector proven to work on a real container. Contribution rules \u2014 accept ONLY: `detector` (required, with installed_via + method-specific fields + repo/tag_regex), optional `alt_detectors` (cross-method fallbacks), optional `file_fallbacks` (same-method secondary paths). REJECT anything that identifies a host: no IP addresses, no VMIDs, no hostnames, no `evidence` blocks with those fields. Report reproduction context in the PR description instead \u2014 the committed JSON must stay generic.", "notes": "Runtime-verified detectors for the LXC app catalog. Every entry is a detector proven to work on a real container, either in maintainer testing or a reviewed community confirmation. Contribution rules \u2014 accept ONLY: `detector` (required, with installed_via + method-specific fields + repo/tag_regex), optional `alt_detectors` (cross-method fallbacks), optional `file_fallbacks` (same-method secondary paths). REJECT anything that identifies a host: no IP addresses, no VMIDs, no hostnames, no `evidence` blocks with those fields. Report reproduction context in the PR description instead \u2014 the committed JSON must stay generic.",
"apps": { "apps": {
"docker": {
"operational": true,
"install_scope": [
"community-script",
"manual"
],
"detector": {
"installed_via": "binary",
"binary_path": "/usr/bin/docker",
"binary_args": [
"--version"
],
"repo": "moby/moby",
"github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
}
},
"portainer": {
"operational": true,
"name": "Portainer",
"install_scope": [
"docker"
],
"detector": {
"installed_via": "docker_exec",
"container_name": "portainer",
"binary_path": "/portainer",
"binary_args": [
"--version"
],
"repo": "portainer/portainer",
"github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
},
"default_ports": [
9000,
9443
],
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/portainer.webp",
"website": "https://www.portainer.io/"
},
"audiobookshelf": {
"operational": true,
"name": "Audiobookshelf",
"install_scope": [
"community-script",
"manual-if-same-package"
],
"evidence": [
"community-confirmed:github-discussion-306"
],
"detector": {
"installed_via": "dpkg",
"package": "audiobookshelf",
"repo": "advplyr/audiobookshelf",
"github_source": "releases",
"tag_regex": "(?i)(?:v|release[-_/]?)?(\\d+(?:\\.\\d+){1,3}(?:[-+._][0-9A-Za-z.-]+)?)"
}
},
"gitea": {
"operational": true,
"name": "Gitea",
"install_scope": [
"community-script",
"manual-if-same-package"
],
"evidence": [
"community-confirmed:github-discussion-306"
],
"detector": {
"installed_via": "apk",
"package": "gitea",
"repo": "go-gitea/gitea",
"github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
}
},
"dispatcharr": {
"operational": true,
"name": "Dispatcharr",
"install_scope": [
"community-script"
],
"evidence": [
"community-confirmed:github-discussion-306"
],
"detector": {
"installed_via": "file",
"file_path": "/root/.dispatcharr",
"file_regex": "(\\d+\\.\\d+\\.\\d+)",
"repo": "Dispatcharr/Dispatcharr",
"github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
}
},
"kavita": {
"operational": true,
"name": "Kavita",
"install_scope": [
"community-script"
],
"evidence": [
"community-confirmed:github-discussion-306"
],
"detector": {
"installed_via": "file",
"file_path": "/root/.kavita",
"file_regex": "(\\d+\\.\\d+\\.\\d+)",
"repo": "Kareadita/Kavita",
"github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
}
},
"qbittorrent": { "qbittorrent": {
"detector": { "detector": {
"installed_via": "binary", "installed_via": "binary",
@@ -29,8 +142,7 @@
] ]
}, },
"openwebui": { "openwebui": {
"operational": false, "operational": true,
"remove_from_v1": true,
"detector": { "detector": {
"installed_via": "python_dist", "installed_via": "python_dist",
"python_path": "/root/.local/share/uv/tools/open-webui/bin/python", "python_path": "/root/.local/share/uv/tools/open-webui/bin/python",
@@ -47,6 +159,15 @@
"tag_regex": "(\\d+\\.\\d+\\.\\d+)" "tag_regex": "(\\d+\\.\\d+\\.\\d+)"
} }
}, },
"jellyfin": {
"detector": {
"installed_via": "dpkg",
"package": "jellyfin",
"repo": "jellyfin/jellyfin",
"github_source": "releases",
"tag_regex": "(?i)(?:v|release[-_/]?)?(\\d+(?:\\.\\d+){1,3}(?:[-+._][0-9A-Za-z.-]+)?)"
}
},
"adguard": { "adguard": {
"detector": { "detector": {
"installed_via": "binary", "installed_via": "binary",
@@ -205,6 +326,19 @@
"github_source": "releases", "github_source": "releases",
"tag_regex": "v?(\\d+\\.\\d+\\.\\d+)" "tag_regex": "v?(\\d+\\.\\d+\\.\\d+)"
} }
},
"searxng": {
"operational": true,
"install_scope": [
"community-script",
"manual-if-same-venv"
],
"detector": {
"installed_via": "python_dist",
"python_path": "/usr/local/searxng/searx-pyenv/bin/python",
"distribution": "searxng",
"installed_regex": "(\\d+\\.\\d+\\.\\d+\\+[0-9a-f]+)"
}
} }
} }
} }
+229
View File
@@ -190,6 +190,215 @@ cleanup_broken_gasket_dkms() {
esac esac
} }
# ============================================================
# Orphan gasket-dkms detection and assisted cleanup
# ============================================================
# The legacy Coral installer (`scripts/install_coral_pve.sh`, retired
# in April 2026) unconditionally installed the gasket-dkms .deb even
# on USB-only hosts. On modern kernels (6.12+) the upstream `gasket
# 1.0` source no longer compiles, so DKMS autoinstall fails, dpkg
# leaves the package half-configured, and every subsequent apt-get
# call errors out.
#
# On hosts without Coral PCIe/M.2 hardware, this package is pure
# residue with no functional purpose — the Coral USB path uses
# libedgetpu1 in userspace and does not need the kernel driver.
# We detect that combination (gasket-dkms present + no PCIe device
# on the bus) and offer explicit, opt-in cleanup.
#
# Design guardrails:
# * Only offered when CORAL_PCIE_COUNT == 0. Never runs on hosts
# with a Coral PCIe/M.2 device present, even if the package is
# broken — those users need the package, and the fix is a
# rebuild (via `install_gasket_apex_dkms`), not a purge.
# * User confirmation always required — nothing removes silently.
# * The Coral USB path (libedgetpu1-std / -max) is never touched.
# Set by `detect_orphan_gasket_dkms`. Empty when no orphan state
# is present; otherwise one of "healthy_orphan" (package installed
# cleanly but hardware absent) or "broken_orphan" (package in a
# half-configured / half-installed / unpacked state and hardware
# absent — this is DavidOliMar's case and blocks apt).
GASKET_ORPHAN_STATE=""
detect_orphan_gasket_dkms() {
GASKET_ORPHAN_STATE=""
# Hardware present -> not orphan, never touched by this flow.
[[ "$CORAL_PCIE_COUNT" -gt 0 ]] && return 0
local pkg_status
pkg_status=$(dpkg-query -W -f='${Status}' gasket-dkms 2>/dev/null || echo "")
[[ -z "$pkg_status" ]] && return 0 # package not installed at all
if [[ "$pkg_status" == *"ok installed"* ]]; then
GASKET_ORPHAN_STATE="healthy_orphan"
elif [[ "$pkg_status" == *"half-configured"* \
|| "$pkg_status" == *"half-installed"* \
|| "$pkg_status" == *"unpacked"* \
|| "$pkg_status" == *"failed-config"* \
|| "$pkg_status" == *"reinst-required"* ]]; then
GASKET_ORPHAN_STATE="broken_orphan"
fi
}
cleanup_orphan_gasket_dkms() {
# Return codes:
# 0 cleanup completed and every final verification passed
# 1 operator cancelled before any change was made
# 2 cleanup ran, but dpkg/DKMS could not be verified as healthy
local msg=""
msg+="\n$(translate 'A legacy gasket-dkms package was found on this host, but no Coral M.2 / PCIe hardware is present.')\n\n"
msg+="$(translate 'This package was installed by older versions of the ProxMenux Coral installer that placed the M.2 kernel driver on every system, including USB-only setups. It is not needed for Coral USB devices, which use libedgetpu1-std / libedgetpu1-max only.')\n\n"
if [[ "$GASKET_ORPHAN_STATE" == "broken_orphan" ]]; then
msg+="\Z1\Zb$(translate 'The package is currently in a broken state and is blocking apt updates on this system.')\Zn\n\n"
fi
msg+="\Zb$(translate 'This cleanup will:')\Zn\n"
msg+="$(translate 'Purge the gasket-dkms package')\n"
msg+="$(translate 'Remove every registered gasket DKMS version')\n"
msg+="$(translate 'Run apt-get install -f to complete any pending package configurations')\n\n"
if [[ "$CORAL_USB_COUNT" -gt 0 || "$CORAL_USB_INSTALLED" == "true" ]]; then
msg+="\Z2$(translate 'Your Coral USB device and its runtime (libedgetpu1) will NOT be affected.')\Zn\n\n"
fi
msg+="$(translate 'If you have a Coral M.2 / PCIe device that is physically installed but not detected by lspci, cancel here and check your hardware first before proceeding.')\n\n"
msg+="\Zb$(translate 'Do you want to proceed with the cleanup?')\Zn"
if ! dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'Legacy gasket-dkms detected')" \
--defaultno --yesno "$msg" 24 84; then
return 1
fi
show_proxmenux_logo
msg_title "$(translate 'Cleanup legacy gasket-dkms')"
export DEBIAN_FRONTEND=noninteractive
msg_info "$(translate 'Purging gasket-dkms package...')"
# Try the clean apt path first; fall back to dpkg force flags if the
# package state prevents apt from resolving the removal itself.
if ! apt-get remove --purge -y gasket-dkms >>"$LOG_FILE" 2>&1; then
dpkg --remove --force-remove-reinstreq gasket-dkms >>"$LOG_FILE" 2>&1 || true
dpkg --purge --force-all gasket-dkms >>"$LOG_FILE" 2>&1 || true
fi
# A host can retain more than the historical gasket/1.0 entry. Read
# every version known by DKMS and also include stale version trees
# that a broken package configuration may have left behind.
local versions=""
local version=""
local dkms_remove_failed=0
if command -v dkms >/dev/null 2>&1; then
versions=$({
dkms status 2>/dev/null \
| awk -F'[,/ ]+' '/^gasket/ {print $2}'
if [[ -d /var/lib/dkms/gasket ]]; then
find /var/lib/dkms/gasket -mindepth 1 -maxdepth 1 -type d \
-exec basename {} \; 2>/dev/null
fi
} | sed '/^$/d' | sort -u)
if [[ -n "$versions" ]]; then
msg_info "$(translate 'Removing every registered gasket DKMS version...')"
while IFS= read -r version; do
[[ -z "$version" ]] && continue
if ! dkms remove -m gasket -v "$version" --all >>"$LOG_FILE" 2>&1; then
dkms_remove_failed=1
fi
done <<<"$versions"
if [[ "$dkms_remove_failed" -eq 0 ]]; then
msg_ok "$(translate 'DKMS registrations removed.')"
else
msg_warn "$(translate 'Some DKMS removals reported errors; final verification will determine the result.')"
fi
fi
fi
local repair_failed=0
msg_info "$(translate 'Completing pending package configurations...')"
if apt-get install -f -y >>"$LOG_FILE" 2>&1; then
msg_ok "$(translate 'Package configurations completed.')"
else
repair_failed=1
msg_warn "$(translate 'Some packages still need attention; review') ${LOG_FILE}"
fi
# Final verification is authoritative. Any dpkg state whose second
# character is not "n" (not installed) or "c" (only config files)
# still represents package payload or unfinished package work.
local package_state=""
local package_remnant=""
local dkms_remnant=""
local audit_output=""
package_state=$(dpkg -l gasket-dkms 2>/dev/null \
| awk '$2 == "gasket-dkms" {print $1; exit}')
if [[ -n "$package_state" && ! "$package_state" =~ ^.[nc] ]]; then
package_remnant="$package_state"
fi
if command -v dkms >/dev/null 2>&1; then
dkms_remnant=$(dkms status 2>/dev/null \
| grep -E '^gasket([,/ ]|$)' \
| head -n1)
fi
audit_output=$(dpkg --audit 2>&1 || true)
if [[ -n "$audit_output" ]]; then
{
echo "---- dpkg --audit after legacy gasket-dkms cleanup ----"
printf '%s\n' "$audit_output"
} >>"$LOG_FILE"
fi
if [[ -n "$package_remnant" ]]; then
repair_failed=1
msg_warn "$(translate 'gasket-dkms is still reported by dpkg in state:') ${package_remnant}. $(translate 'Manual review is required.')"
else
msg_ok "$(translate 'gasket-dkms has been fully removed from this system.')"
fi
if [[ -n "$dkms_remnant" ]]; then
repair_failed=1
msg_warn "$(translate 'A gasket DKMS registration is still present:') ${dkms_remnant}"
else
msg_ok "$(translate 'No gasket DKMS registrations remain.')"
fi
if [[ -n "$audit_output" ]]; then
repair_failed=1
msg_warn "$(translate 'dpkg still reports unfinished package work; review') ${LOG_FILE}"
else
msg_ok "$(translate 'The dpkg package database is clean.')"
fi
# Clear the component marker only after gasket itself is confirmed
# absent. A separate dpkg audit problem must still make the overall
# operation fail, but should not leave a false Coral PCIe component.
if [[ -z "$package_remnant" && -z "$dkms_remnant" ]]; then
if declare -f update_component_status >/dev/null 2>&1; then
update_component_status "coral_driver" "removed" "" "gpu" '{}' >/dev/null 2>&1 || true
fi
rm -f /var/lib/proxmenux/coral_gasket_version 2>/dev/null || true
fi
if [[ "$repair_failed" -ne 0 ]]; then
echo
msg_error "$(translate 'Legacy gasket-dkms cleanup could not be verified as complete.')"
msg_warn "$(translate 'No reboot was started. Review the log before retrying:') ${LOG_FILE}"
return 2
fi
echo
msg_success "$(translate 'Cleanup completed. A reboot is recommended to fully apply pending kernel package configurations.')"
restart_prompt
return 0
}
clone_gasket_sources() { clone_gasket_sources() {
# Primary: feranick/gasket-driver — community fork, actively maintained, # Primary: feranick/gasket-driver — community fork, actively maintained,
# carries patches for kernel 6.10/6.12/6.13. # carries patches for kernel 6.10/6.12/6.13.
@@ -655,10 +864,12 @@ restart_prompt() {
# Main orchestrator # Main orchestrator
# ============================================================ # ============================================================
main() { main() {
local cleanup_rc=0
: >"$LOG_FILE" : >"$LOG_FILE"
detect_coral_hardware detect_coral_hardware
detect_coral_install_state detect_coral_install_state
detect_orphan_gasket_dkms
# No hardware AND no leftover install → nothing to do. # No hardware AND no leftover install → nothing to do.
if [[ "$CORAL_PCIE_COUNT" -eq 0 && "$CORAL_USB_COUNT" -eq 0 ]] \ if [[ "$CORAL_PCIE_COUNT" -eq 0 && "$CORAL_USB_COUNT" -eq 0 ]] \
@@ -667,6 +878,24 @@ main() {
exit 0 exit 0
fi fi
# Legacy gasket-dkms package left behind by the retired installer
# (see detect_orphan_gasket_dkms header). Offer explicit cleanup
# before the normal action menu so the user sees a curated fix
# instead of a broken install/remove flow. If the operator cancels,
# we still fall through to the standard menu (they may want to act
# on the USB runtime independently).
if [[ -n "$GASKET_ORPHAN_STATE" ]]; then
if cleanup_orphan_gasket_dkms; then
exit 0
else
cleanup_rc=$?
# Return 1 means the operator cancelled and may still use the
# standard menu. A failed repair must stop here instead of
# continuing as though the package manager were healthy.
[[ "$cleanup_rc" -eq 1 ]] || exit "$cleanup_rc"
fi
fi
# If something is already installed, offer reinstall/uninstall choice. # If something is already installed, offer reinstall/uninstall choice.
# Same UX as nvidia_installer.sh. When nothing is installed yet, # Same UX as nvidia_installer.sh. When nothing is installed yet,
# ACTION="install" automatically. # ACTION="install" automatically.
+136 -31
View File
@@ -10,14 +10,28 @@
# BACKUP — "1" to snapshot with vzdump first, "0" to skip # BACKUP — "1" to snapshot with vzdump first, "0" to skip
# BACKUP_STORAGE — PVE storage name for vzdump (required when BACKUP=1) # BACKUP_STORAGE — PVE storage name for vzdump (required when BACKUP=1)
# RESTART — "1" to `pct reboot` after update, "0" to skip # RESTART — "1" to `pct reboot` after update, "0" to skip
# UPDATE_COMMAND — optional; user-defined bash string. When set # RUN_HELPER — "1" to run the verified community-scripts
# updater referenced by /usr/bin/update, "0"
# to leave it alone. Never inferred from names.
# UPDATE_COMMAND — optional user-defined bash string. When set
# and TARGET is "app" or "both", the script # and TARGET is "app" or "both", the script
# runs this VIA sh -c inside the CT instead of # runs it VIA sh -c inside the CT. A custom
# /usr/bin/update. This IS the one place we # command always replaces RUN_HELPER for safety.
# This IS the one place we
# intentionally use sh -c with a variable # intentionally use sh -c with a variable
# payload — the threat model matches "user # payload — the threat model matches "user
# typed it via pct exec themselves"; ProxMenux # typed it via pct exec themselves"; ProxMenux
# does not compose or interpret the command. # does not compose or interpret the command.
# ALLOW_HELPER_WITH_CUSTOM — "1" only for an explicit multi-app plan
# where RUN_HELPER belongs to one registered app and
# UPDATE_COMMAND contains other registered apps. The
# default "0" preserves the custom-replaces-helper rule
# for single-app and legacy callers.
# DOCKER_STANDALONE_TARGETS — optional comma-separated Docker container
# names. Each is recreated transactionally by the
# protected host-side Docker recreation helper.
# UPDATE_DOCKER_ENGINE — "1" to update only the installed Docker Engine
# package stack, without upgrading unrelated OS packages.
# #
# Exit codes: # Exit codes:
# 0 everything requested completed OK # 0 everything requested completed OK
@@ -26,8 +40,9 @@
# 3 pre-update backup failed (abort so the user still has a rollback) # 3 pre-update backup failed (abort so the user still has a rollback)
# 4 OS update failed OR OS family not supported for automated updates # 4 OS update failed OR OS family not supported for automated updates
# 5 TARGET=app requested but no update method (neither UPDATE_COMMAND # 5 TARGET=app requested but no update method (neither UPDATE_COMMAND
# nor /usr/bin/update) available in the CT # nor explicitly-enabled verified helper) available in the CT
# 6 post-update restart failed # 6 post-update restart failed
# 7 another ProxMenux update is already running for this CT
# #
# The frontend surfaces exit code + duration in a follow-up POST to # The frontend surfaces exit code + duration in a follow-up POST to
# /api/lxc-updates/<vmid>/applied so the notification event fires with # /api/lxc-updates/<vmid>/applied so the notification event fires with
@@ -40,6 +55,41 @@ set -o pipefail
: "${TARGET:?TARGET is required}" : "${TARGET:?TARGET is required}"
BACKUP="${BACKUP:-0}" BACKUP="${BACKUP:-0}"
RESTART="${RESTART:-0}" RESTART="${RESTART:-0}"
RUN_HELPER="${RUN_HELPER:-0}"
UPDATE_COMMAND="${UPDATE_COMMAND:-}"
ALLOW_HELPER_WITH_CUSTOM="${ALLOW_HELPER_WITH_CUSTOM:-0}"
DOCKER_STANDALONE_TARGETS="${DOCKER_STANDALONE_TARGETS:-}"
UPDATE_DOCKER_ENGINE="${UPDATE_DOCKER_ENGINE:-0}"
if [[ ! "$VMID" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: VMID must be a positive integer." >&2
exit 1
fi
if [[ "$TARGET" != "os" && "$TARGET" != "app" && "$TARGET" != "both" ]]; then
echo "ERROR: TARGET must be os, app, or both." >&2
exit 4
fi
if [[ "$RUN_HELPER" != "0" && "$RUN_HELPER" != "1" ]]; then
echo "ERROR: RUN_HELPER must be 0 or 1." >&2
exit 5
fi
if [[ "$ALLOW_HELPER_WITH_CUSTOM" != "0" && "$ALLOW_HELPER_WITH_CUSTOM" != "1" ]]; then
echo "ERROR: ALLOW_HELPER_WITH_CUSTOM must be 0 or 1." >&2
exit 5
fi
if [[ "$UPDATE_DOCKER_ENGINE" != "0" && "$UPDATE_DOCKER_ENGINE" != "1" ]]; then
echo "ERROR: UPDATE_DOCKER_ENGINE must be 0 or 1." >&2
exit 5
fi
# One update per CT at a time, regardless of whether it came from the
# UI or the scheduler. The descriptor remains open for this process.
LOCK_DIR="${PROXMENUX_LOCK_DIR:-/run/lock}"
exec 9>"${LOCK_DIR}/proxmenux-lxc-update-${VMID}.lock"
if ! flock -n 9; then
echo "ERROR: another ProxMenux update is already running for CT $VMID." >&2
exit 7
fi
STARTED_AT=$(date -Iseconds) STARTED_AT=$(date -Iseconds)
NODE=$(hostname) NODE=$(hostname)
@@ -48,6 +98,7 @@ echo "Started: $STARTED_AT"
echo "Target: $TARGET" echo "Target: $TARGET"
echo "Backup: $BACKUP${BACKUP_STORAGE:+ (storage: $BACKUP_STORAGE)}" echo "Backup: $BACKUP${BACKUP_STORAGE:+ (storage: $BACKUP_STORAGE)}"
echo "Restart: $RESTART" echo "Restart: $RESTART"
echo "Helper: $RUN_HELPER"
echo echo
# 1) CT must exist on this node. # 1) CT must exist on this node.
@@ -56,14 +107,35 @@ if ! pct list | awk 'NR>1 {print $1}' | grep -qE "^${VMID}$"; then
exit 1 exit 1
fi fi
# 2) CT must be running for pct exec. Auto-start stopped CTs. # 2) CT must be running for pct exec. Auto-start stopped CTs, then
# restore their original stopped state on every exit path.
STATE=$(pct status "$VMID" | awk '{print $2}') STATE=$(pct status "$VMID" | awk '{print $2}')
STARTED_BY_PROXMENUX=0
restore_original_state() {
local rc=$?
trap - EXIT INT TERM
if [[ "$STARTED_BY_PROXMENUX" == "1" ]]; then
echo
echo "Restoring original state: stopping CT $VMID"
if ! pct shutdown "$VMID" --timeout 60; then
echo "ERROR: update finished but CT $VMID could not be returned to its original stopped state." >&2
if [[ "$rc" -eq 0 ]]; then
rc=6
fi
fi
fi
exit "$rc"
}
trap restore_original_state EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if [[ "$STATE" != "running" ]]; then if [[ "$STATE" != "running" ]]; then
echo "CT is $STATE. Starting it before applying updates…" echo "CT is $STATE. Starting it before applying updates…"
if ! pct start "$VMID"; then if ! pct start "$VMID"; then
echo "ERROR: failed to start CT $VMID." >&2 echo "ERROR: failed to start CT $VMID." >&2
exit 2 exit 2
fi fi
STARTED_BY_PROXMENUX=1
# give the CT a moment for services to come up # give the CT a moment for services to come up
sleep 3 sleep 3
fi fi
@@ -123,41 +195,55 @@ if [[ "$TARGET" == "os" || "$TARGET" == "both" ]]; then
echo echo
fi fi
# 6) Application update. Precedence: # 6) Application update. Explicit methods only:
# a) /usr/bin/update present (community-scripts convention) # a) RUN_HELPER=1 + a valid /usr/bin/update wrapper
# runs the community-scripts helper FROM THE HOST with CTID # parses the ct/<slug>.sh URL from the wrapper, canonicalises it
# env var. Their build.func framework requires CTID + host-only # to the official repository, then runs the current helper
# `pveversion`, so `pct exec ... /usr/bin/update` inside the CT # inside the CT with PHS_SILENT=1.
# always fails ("You need to set 'CTID' variable"). We parse # b) UPDATE_COMMAND set → run it verbatim via `sh -c`
# the ct/<slug>.sh URL from /usr/bin/update and re-fetch it
# here with CTID set. PHS_SILENT=1 keeps it non-interactive.
# b) UPDATE_COMMAND env var set → run it verbatim via `sh -c`
# inside the CT. The one intentional shell-exec-with-variable # inside the CT. The one intentional shell-exec-with-variable
# in ProxMenux — see header comment for threat-model rationale. # in ProxMenux — see header comment for threat-model rationale.
# Both can run in the same invocation: the helper first (if # UPDATE_COMMAND always wins if a legacy caller also sets RUN_HELPER.
# present), then the per-app custom commands. # A hostname/tag/cache guess is never executable evidence.
if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then
APP_METHOD_RAN=0 APP_METHOD_RAN=0
if [[ -n "$UPDATE_COMMAND" && "$RUN_HELPER" == "1" && "$ALLOW_HELPER_WITH_CUSTOM" != "1" ]]; then
echo "Custom update command configured; skipping Proxmox VE Helper-Scripts updater."
RUN_HELPER=0
fi
if [[ "$UPDATE_DOCKER_ENGINE" == "1" ]]; then
echo "--- Updating Docker Engine only ---"
if ! python3 /usr/local/share/proxmenux/monitor-app/usr/bin/update_docker_engine.py \
--vmid "$VMID"; then
echo "ERROR: Docker Engine update failed." >&2
APP_FAILED=1
fi
APP_METHOD_RAN=1
echo
fi
if [[ "$RUN_HELPER" == "1" ]]; then
UPDATE_URL="" UPDATE_URL=""
RESOLVED_SLUG="" RESOLVED_SLUG=""
if pct exec "$VMID" -- test -f /usr/bin/update 2>/dev/null; then if pct exec "$VMID" -- test -f /usr/bin/update 2>/dev/null; then
UPDATE_URL=$(pct exec "$VMID" -- cat /usr/bin/update 2>/dev/null | grep -oE 'https?://[^"'"'"' ]+ct/[a-zA-Z0-9._-]+\.sh' | head -1) UPDATE_URL=$(pct exec "$VMID" -- cat /usr/bin/update 2>/dev/null | grep -oE 'https?://[^"'"'"' ]+ct/[a-zA-Z0-9._-]+\.sh' | head -1)
RESOLVED_SLUG=$(echo "$UPDATE_URL" | sed -nE 's|.*/ct/([a-zA-Z0-9._-]+)\.sh$|\1|p') RESOLVED_SLUG=$(echo "$UPDATE_URL" | sed -nE 's|.*/ct/([a-zA-Z0-9._-]+)\.sh$|\1|p')
fi fi
# HELPER_SLUG env is a passthrough from the backend when the CT no case "$RESOLVED_SLUG" in
# longer carries /usr/bin/update (older installs where the file was alpine|archlinux|archlinux-vm|debian|fedora|gentoo|opensuse|ubuntu)
# removed) but the community-scripts slug is known via hostname echo "ERROR: /usr/bin/update references the base-OS helper '$RESOLVED_SLUG', not an application updater." >&2
# match against the helpers_cache. Lets us run the same host-side APP_FAILED=1
# updater without requiring the on-CT marker file. RESOLVED_SLUG=""
if [[ -z "$RESOLVED_SLUG" && -n "$HELPER_SLUG" ]]; then ;;
if [[ "$HELPER_SLUG" =~ ^[a-zA-Z0-9._-]+$ ]]; then esac
RESOLVED_SLUG="$HELPER_SLUG" if [[ -z "$RESOLVED_SLUG" ]]; then
UPDATE_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${RESOLVED_SLUG}.sh" if [[ "$APP_FAILED" -eq 0 ]]; then
echo "ERROR: RUN_HELPER=1 but /usr/bin/update contains no valid community-scripts app reference." >&2
APP_FAILED=1
fi
else else
echo "WARN: HELPER_SLUG contains invalid characters — ignored." >&2 # Never execute the arbitrary URL embedded in the CT. The slug is
fi # constrained by the parser; fetch the canonical upstream path.
fi UPDATE_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${RESOLVED_SLUG}.sh"
if [[ -n "$UPDATE_URL" && -n "$RESOLVED_SLUG" ]]; then
echo "--- Running community-scripts helper (slug: $RESOLVED_SLUG) ---" echo "--- Running community-scripts helper (slug: $RESOLVED_SLUG) ---"
# Community-scripts' build.func in start() dispatches on # Community-scripts' build.func in start() dispatches on
# `command -v pveversion`: present → install_script (whiptail # `command -v pveversion`: present → install_script (whiptail
@@ -185,6 +271,7 @@ if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then
APP_METHOD_RAN=1 APP_METHOD_RAN=1
echo echo
fi fi
fi
if [[ -n "$UPDATE_COMMAND" ]]; then if [[ -n "$UPDATE_COMMAND" ]]; then
echo "--- Running user-defined update command ---" echo "--- Running user-defined update command ---"
echo "\$ $UPDATE_COMMAND" echo "\$ $UPDATE_COMMAND"
@@ -195,9 +282,27 @@ if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then
APP_METHOD_RAN=1 APP_METHOD_RAN=1
echo echo
fi fi
if [[ -n "$DOCKER_STANDALONE_TARGETS" ]]; then
IFS=',' read -r -a DOCKER_TARGETS <<< "$DOCKER_STANDALONE_TARGETS"
for DOCKER_CONTAINER in "${DOCKER_TARGETS[@]}"; do
if [[ ! "$DOCKER_CONTAINER" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "ERROR: invalid Docker container target '$DOCKER_CONTAINER'." >&2
APP_FAILED=1
continue
fi
echo "--- Recreating standalone Docker container: $DOCKER_CONTAINER ---"
if ! python3 /usr/local/share/proxmenux/monitor-app/usr/bin/recreate_docker_container.py \
--vmid "$VMID" --container "$DOCKER_CONTAINER"; then
echo "ERROR: protected Docker recreation failed for '$DOCKER_CONTAINER'." >&2
APP_FAILED=1
fi
APP_METHOD_RAN=1
echo
done
fi
if [[ "$APP_METHOD_RAN" -eq 0 ]]; then if [[ "$APP_METHOD_RAN" -eq 0 ]]; then
if [[ "$TARGET" == "app" ]]; then if [[ "$TARGET" == "app" ]]; then
echo "ERROR: TARGET=app but no update method (UPDATE_COMMAND unset AND /usr/bin/update missing) in CT $VMID." >&2 echo "ERROR: TARGET=app but no update method was explicitly selected for CT $VMID." >&2
exit 5 exit 5
else else
echo "No app update method available in this CT — skipping app update step." echo "No app update method available in this CT — skipping app update step."
@@ -209,7 +314,7 @@ fi
# 7) If either branch failed, abort here BEFORE the optional reboot so # 7) If either branch failed, abort here BEFORE the optional reboot so
# the CT stays in the pre-update state and the user can inspect it. # the CT stays in the pre-update state and the user can inspect it.
if (( OS_FAILED || APP_FAILED )); then if (( OS_FAILED || APP_FAILED )); then
echo "=== Update FAILED — CT left running for inspection. ===" echo "=== Update FAILED. ==="
exit 4 exit 4
fi fi