mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-09 09:16:20 +00:00
Merge pull request #282 from Vaso73/i18n/slovak-lxc-updates-app-editor
i18n(lxc): localize updates and fix terminal completion
This commit is contained in:
@@ -275,7 +275,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
setSuggestions(s)
|
setSuggestions(s)
|
||||||
} catch { /* non-fatal */ }
|
} catch { /* non-fatal */ }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Could not load app configuration")
|
setError(e?.message || t("vmLxc.appEditor.loadFailed"))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
setSidecar(r)
|
setSidecar(r)
|
||||||
onChange?.()
|
onChange?.()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Check failed")
|
setError(e?.message || t("vmLxc.appEditor.checkFailed"))
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAppId(null)
|
setBusyAppId(null)
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
await load()
|
await load()
|
||||||
onChange?.()
|
onChange?.()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Delete failed")
|
setError(e?.message || t("vmLxc.appEditor.deleteFailed"))
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAppId(null)
|
setBusyAppId(null)
|
||||||
}
|
}
|
||||||
@@ -532,7 +532,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
})
|
})
|
||||||
setSidecar(r)
|
setSidecar(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Could not dismiss detection")
|
setError(e?.message || t("vmLxc.appEditor.dismissFailed"))
|
||||||
await load() // resync on failure
|
await load() // resync on failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,7 +554,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
})
|
})
|
||||||
setSidecar(r)
|
setSidecar(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Could not restore detection")
|
setError(e?.message || t("vmLxc.appEditor.restoreFailed"))
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,7 +571,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
// in an Alpine CT). When we add more OCI apps we'll swap this to
|
// in an Alpine CT). When we add more OCI apps we'll swap this to
|
||||||
// a lookup keyed on managed_oci_app_id → catalog metadata.
|
// a lookup keyed on managed_oci_app_id → catalog metadata.
|
||||||
const isSecureGateway = managed.managed_oci_app_id === "secure-gateway"
|
const isSecureGateway = managed.managed_oci_app_id === "secure-gateway"
|
||||||
const displayName = isSecureGateway ? "Secure Gateway" : (managed.name || "Managed app")
|
const displayName = isSecureGateway ? "Secure Gateway" : (managed.name || t("vmLxc.appEditor.managedApp"))
|
||||||
const displaySubtitle = isSecureGateway ? "Tailscale VPN Gateway" : ""
|
const displaySubtitle = isSecureGateway ? "Tailscale VPN Gateway" : ""
|
||||||
// Two variants — the selfh.st mark (dark logo on light bg) reads
|
// Two variants — the selfh.st mark (dark logo on light bg) reads
|
||||||
// better in light mode; the homarr-labs "-light" variant (light
|
// better in light mode; the homarr-labs "-light" variant (light
|
||||||
@@ -585,7 +585,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
: ""
|
: ""
|
||||||
const upstreamName = isSecureGateway ? "Tailscale" : ""
|
const upstreamName = isSecureGateway ? "Tailscale" : ""
|
||||||
const repo = isSecureGateway ? "tailscale/tailscale" : ""
|
const repo = isSecureGateway ? "tailscale/tailscale" : ""
|
||||||
const methodLine = isSecureGateway ? "apk · tailscale · managed" : "managed"
|
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 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)
|
||||||
@@ -646,7 +646,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
<div className="text-xs text-muted-foreground mt-0.5">{methodLine}</div>
|
<div className="text-xs text-muted-foreground mt-0.5">{methodLine}</div>
|
||||||
{managed.checked_at && (
|
{managed.checked_at && (
|
||||||
<div className="text-[10px] text-muted-foreground/80 mt-0.5">
|
<div className="text-[10px] text-muted-foreground/80 mt-0.5">
|
||||||
Checked {new Date(managed.checked_at).toLocaleString([], { dateStyle: "short", timeStyle: "short" })}
|
{t("vmLxc.appEditor.checkedAt", { date: new Date(managed.checked_at).toLocaleString([], { dateStyle: "short", timeStyle: "short" }) })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{repo && (
|
{repo && (
|
||||||
@@ -693,7 +693,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{repo && (
|
{repo && (
|
||||||
<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">
|
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
Latest upstream
|
{t("vmLxc.appEditor.latestUpstream")}
|
||||||
</div>
|
</div>
|
||||||
<div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
|
<div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
|
||||||
{managed.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
|
{managed.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
|
||||||
@@ -729,7 +729,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||||
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
||||||
Loading applications…
|
{t("vmLxc.appEditor.loadingApplications")}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -798,7 +798,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
onChange={(e) => { setName(e.target.value); setPickerOpen(true) }}
|
onChange={(e) => { setName(e.target.value); setPickerOpen(true) }}
|
||||||
onFocus={() => setPickerOpen(true)}
|
onFocus={() => setPickerOpen(true)}
|
||||||
onBlur={() => setTimeout(() => setPickerOpen(false), 150)}
|
onBlur={() => setTimeout(() => setPickerOpen(false), 150)}
|
||||||
placeholder={suggestions?.name_suggestion || "Type to search 700+ apps, or type your own"}
|
placeholder={suggestions?.name_suggestion || t("vmLxc.appEditor.nameSearchPlaceholder")}
|
||||||
maxLength={64}
|
maxLength={64}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -817,7 +817,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="absolute z-20 left-0 right-0 mt-1 max-h-72 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
|
<div className="absolute z-20 left-0 right-0 mt-1 max-h-72 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
|
||||||
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground border-b border-border/60">
|
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground border-b border-border/60">
|
||||||
{matches.length === 20 ? t("vmLxc.appEditor.top20Matches") : `${matches.length} match${matches.length === 1 ? "" : "es"}`}
|
{matches.length === 20 ? t("vmLxc.appEditor.top20Matches") : t("vmLxc.appEditor.matchCount", { count: matches.length })}
|
||||||
</div>
|
</div>
|
||||||
{matches.map((c) => (
|
{matches.map((c) => (
|
||||||
<button
|
<button
|
||||||
@@ -876,8 +876,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
<div className="text-sm text-foreground truncate">{c.name}</div>
|
<div className="text-sm text-foreground truncate">{c.name}</div>
|
||||||
<div className="text-[10px] text-muted-foreground">
|
<div className="text-[10px] text-muted-foreground">
|
||||||
{c.slug}
|
{c.slug}
|
||||||
{c.default_port ? ` · port ${c.default_port}` : ""}
|
{c.default_port ? ` · ${t("vmLxc.appEditor.catalogPort", { port: c.default_port })}` : ""}
|
||||||
{c.has_tracking ? " · tracking available" : ""}
|
{c.has_tracking ? ` · ${t("vmLxc.appEditor.trackingAvailable")}` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -894,7 +894,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
app card header. */}
|
app card header. */}
|
||||||
<div className="pt-2 border-t border-border/50">
|
<div className="pt-2 border-t border-border/50">
|
||||||
<Label htmlFor="app-logo" className="text-xs uppercase tracking-wider text-muted-foreground">
|
<Label htmlFor="app-logo" className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
Logo URL <span className="normal-case tracking-normal text-[10px] opacity-70">(optional)</span>
|
{t("vmLxc.appEditor.logoUrlLabel")} <span className="normal-case tracking-normal text-[10px] opacity-70">{t("vmLxc.appEditor.optionalSuffix")}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="app-logo"
|
id="app-logo"
|
||||||
@@ -913,11 +913,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
<div className="pt-2 border-t border-border/50">
|
<div className="pt-2 border-t border-border/50">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
Web links
|
{t("vmLxc.appEditor.webLinks")}
|
||||||
</Label>
|
</Label>
|
||||||
<Button type="button" variant="ghost" size="sm" onClick={addEmptyPort}>
|
<Button type="button" variant="ghost" size="sm" onClick={addEmptyPort}>
|
||||||
<PlusCircle className="h-3.5 w-3.5 mr-1" />
|
<PlusCircle className="h-3.5 w-3.5 mr-1" />
|
||||||
Add port
|
{t("vmLxc.appEditor.addPort")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -928,7 +928,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{suggestable.length > 0 && (
|
{suggestable.length > 0 && (
|
||||||
<div className="mb-2 flex flex-wrap gap-1 items-center">
|
<div className="mb-2 flex flex-wrap gap-1 items-center">
|
||||||
<span className="text-[10px] text-muted-foreground pr-1">
|
<span className="text-[10px] text-muted-foreground pr-1">
|
||||||
Ports detected in the container — click to add:
|
{t("vmLxc.appEditor.detectedPorts")}
|
||||||
</span>
|
</span>
|
||||||
{suggestable.map((p) => (
|
{suggestable.map((p) => (
|
||||||
<button
|
<button
|
||||||
@@ -945,8 +945,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
|
|
||||||
{draft.ports.length === 0 && suggestable.length === 0 && (
|
{draft.ports.length === 0 && suggestable.length === 0 && (
|
||||||
<div className="text-xs text-muted-foreground italic">
|
<div className="text-xs text-muted-foreground italic">
|
||||||
No web ports listening. Use "Add port" if you'd still
|
{t("vmLxc.appEditor.noWebPorts")}
|
||||||
like to register one manually.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1025,15 +1024,15 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-3.5 w-3.5" />
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
Track upstream version (optional)
|
{t("vmLxc.appEditor.trackUpstream")}
|
||||||
{!showAdvanced && !method && (
|
{!showAdvanced && !method && (
|
||||||
<span className="ml-auto text-[10px] normal-case tracking-normal">
|
<span className="ml-auto text-[10px] normal-case tracking-normal">
|
||||||
Off — link only
|
{t("vmLxc.appEditor.trackOff")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!showAdvanced && method && (
|
{!showAdvanced && method && (
|
||||||
<span className="ml-auto text-[10px] normal-case tracking-normal text-emerald-400/80">
|
<span className="ml-auto text-[10px] normal-case tracking-normal text-emerald-400/80">
|
||||||
On · {method}
|
{t("vmLxc.appEditor.trackOn", { method })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -1041,11 +1040,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
Optional. When enabled, ProxMenux checks the installed
|
{t("vmLxc.appEditor.trackHelp")}
|
||||||
version inside the container and compares it against
|
|
||||||
a GitHub repo — you'll see "update available" and get
|
|
||||||
an optional notification. Skip this if you only want
|
|
||||||
a clickable link.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
@@ -1239,7 +1234,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
Runs argv-style inside the CT as root — never through a shell. You are responsible for what the command does. Use <code className="text-foreground/70">installed_regex</code> below to extract the version from the output.
|
{t("vmLxc.appEditor.commandSafetyHelp")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Label htmlFor="app-cmd-regex">{t("vmLxc.appEditor.installedVersionRegexLabel")}</Label>
|
<Label htmlFor="app-cmd-regex">{t("vmLxc.appEditor.installedVersionRegexLabel")}</Label>
|
||||||
@@ -1266,7 +1261,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
Type the version you have installed. If <code className="text-foreground/70">repo</code> is set below, ProxMenux still checks upstream and notifies on updates — after upgrading the app, come back here and update this string.
|
{t("vmLxc.appEditor.manualVersionHelp")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1314,7 +1309,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
How ProxMenux finds the latest version to compare against your installed one.
|
{t("vmLxc.appEditor.upstreamHelp")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1329,7 +1324,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
placeholder={t("vmLxc.appEditor.githubRepoPlaceholder")}
|
placeholder={t("vmLxc.appEditor.githubRepoPlaceholder")}
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
Public GitHub repository where releases or tags are published.
|
{t("vmLxc.appEditor.githubRepoHelp")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1362,7 +1357,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
maxLength={512}
|
maxLength={512}
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
Public JSON endpoint that returns a version somewhere in the payload.
|
{t("vmLxc.appEditor.httpJsonHelp")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1376,7 +1371,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
maxLength={128}
|
maxLength={128}
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1">
|
<div className="text-[10px] text-muted-foreground mt-1">
|
||||||
Dotted path with optional <code className="text-foreground/70">[N]</code> array indices.
|
{t("vmLxc.appEditor.jsonPathHelp")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1478,7 +1473,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3.5 w-3.5 mr-1.5" />
|
<RotateCcw className="h-3.5 w-3.5 mr-1.5" />
|
||||||
Restore
|
{t("vmLxc.appEditor.restoreButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1518,7 +1513,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="flex-[3] sm:flex-none bg-blue-500 hover:bg-blue-600 text-white"
|
className="flex-[3] sm:flex-none bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
>
|
>
|
||||||
<PlusCircle className="h-3.5 w-3.5 mr-1" />
|
<PlusCircle className="h-3.5 w-3.5 mr-1" />
|
||||||
Register
|
{t("vmLxc.appEditor.registerButton")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1717,7 +1712,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{hasUpstream && (
|
{hasUpstream && (
|
||||||
<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">
|
<div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">
|
||||||
Latest upstream
|
{t("vmLxc.appEditor.latestUpstream")}
|
||||||
</div>
|
</div>
|
||||||
<div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
|
<div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
|
||||||
{st?.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
|
{st?.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
|
||||||
@@ -1796,7 +1791,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="h-8 px-3 text-xs rounded-md border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-red-400 transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
|
className="h-8 px-3 text-xs rounded-md border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-red-400 transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
Remove
|
{t("vmLxc.appEditor.removeButton")}
|
||||||
</button>
|
</button>
|
||||||
<div className="ml-auto flex flex-wrap gap-2">
|
<div className="ml-auto flex flex-wrap gap-2">
|
||||||
{tracking && (
|
{tracking && (
|
||||||
@@ -1809,7 +1804,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{busyAppId === app.id
|
{busyAppId === app.id
|
||||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||||
Check
|
{t("vmLxc.appEditor.checkButton")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -1818,7 +1813,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
|
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<Settings2 className="h-3.5 w-3.5" />
|
<Settings2 className="h-3.5 w-3.5" />
|
||||||
Edit fields
|
{t("vmLxc.appEditor.editFieldsButton")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1836,7 +1831,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{apps.length > 0 && unregisteredDetected.length > 0 && (
|
{apps.length > 0 && unregisteredDetected.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs uppercase tracking-wider text-muted-foreground">
|
<div className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
Also detected on this container
|
{t("vmLxc.appEditor.alsoDetectedContainer")}
|
||||||
</div>
|
</div>
|
||||||
{unregisteredDetected.map(renderDetectionChip)}
|
{unregisteredDetected.map(renderDetectionChip)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1857,10 +1852,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
disabled={editMode}
|
disabled={editMode}
|
||||||
>
|
>
|
||||||
<PlusCircle className="h-4 w-4 mr-1.5" />
|
<PlusCircle className="h-4 w-4 mr-1.5" />
|
||||||
Add another application
|
{t("vmLxc.appEditor.addAnotherApplication")}
|
||||||
{hiddenDetections.length > 0 && (
|
{hiddenDetections.length > 0 && (
|
||||||
<span className="ml-2 text-[10px] opacity-70">
|
<span className="ml-2 text-[10px] opacity-70">
|
||||||
· {hiddenDetections.length} hidden
|
· {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1872,12 +1867,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
|
|||||||
{editMode ? (
|
{editMode ? (
|
||||||
<>
|
<>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
Done
|
{t("vmLxc.appEditor.doneButton")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
Edit
|
{t("vmLxc.appEditor.editButton")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -50,12 +50,14 @@ interface ScriptTerminalModalProps {
|
|||||||
description: string
|
description: string
|
||||||
scriptName?: string
|
scriptName?: string
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>
|
||||||
|
completedSuccessfullyMessage?: string
|
||||||
|
completedWithErrorMessage?: (exitCode: number) => string
|
||||||
// Optional callback fired when the script's WebSocket closes
|
// Optional callback fired when the script's WebSocket closes
|
||||||
// (script_runner sends an exit code and then closes). Lets the
|
// (script_runner sends an exit code and then closes). Lets the
|
||||||
// parent auto-dismiss the modal — used by host-backup's Restore
|
// parent auto-dismiss the modal — used by host-backup's Restore
|
||||||
// flow so "Press Enter to close" in the bash script actually
|
// flow so "Press Enter to close" in the bash script actually
|
||||||
// closes the modal without an extra click. Other callers ignore.
|
// closes the modal without an extra click. Other callers ignore.
|
||||||
onComplete?: () => void
|
onComplete?: (exitCode?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScriptTerminalModal({
|
export function ScriptTerminalModal({
|
||||||
@@ -65,6 +67,8 @@ export function ScriptTerminalModal({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
params = { EXECUTION_MODE: "web" },
|
params = { EXECUTION_MODE: "web" },
|
||||||
|
completedSuccessfullyMessage,
|
||||||
|
completedWithErrorMessage,
|
||||||
onComplete,
|
onComplete,
|
||||||
}: ScriptTerminalModalProps) {
|
}: ScriptTerminalModalProps) {
|
||||||
const t = useT()
|
const t = useT()
|
||||||
@@ -85,6 +89,7 @@ export function ScriptTerminalModal({
|
|||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const reconnectAttemptsRef = useRef(0)
|
const reconnectAttemptsRef = useRef(0)
|
||||||
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const completionReceivedRef = useRef(false)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isTablet, setIsTablet] = useState(false)
|
const [isTablet, setIsTablet] = useState(false)
|
||||||
|
|
||||||
@@ -96,6 +101,14 @@ export function ScriptTerminalModal({
|
|||||||
const resizeBarRef = useRef<HTMLDivElement>(null)
|
const resizeBarRef = useRef<HTMLDivElement>(null)
|
||||||
const modalHeightRef = useRef(600)
|
const modalHeightRef = useRef(600)
|
||||||
|
|
||||||
|
const getCompletionMessage = useCallback(
|
||||||
|
(exitCode: number) =>
|
||||||
|
exitCode === 0
|
||||||
|
? (completedSuccessfullyMessage ?? t("scriptTerminal.completedSuccessfully"))
|
||||||
|
: (completedWithErrorMessage?.(exitCode) ?? t("scriptTerminal.completedWithError", { code: exitCode })),
|
||||||
|
[completedSuccessfullyMessage, completedWithErrorMessage, t],
|
||||||
|
)
|
||||||
|
|
||||||
const terminalContainerRef = useRef<HTMLDivElement>(null)
|
const terminalContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const paramsRef = useRef(params)
|
const paramsRef = useRef(params)
|
||||||
|
|
||||||
@@ -106,7 +119,7 @@ export function ScriptTerminalModal({
|
|||||||
|
|
||||||
// Same trick for onComplete — we want the latest callback inside
|
// Same trick for onComplete — we want the latest callback inside
|
||||||
// the ws.onclose handler without re-running the connection effect.
|
// the ws.onclose handler without re-running the connection effect.
|
||||||
const onCompleteRef = useRef<(() => void) | undefined>(undefined)
|
const onCompleteRef = useRef<((exitCode?: number) => void) | undefined>(undefined)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onCompleteRef.current = onComplete
|
onCompleteRef.current = onComplete
|
||||||
}, [onComplete])
|
}, [onComplete])
|
||||||
@@ -167,6 +180,23 @@ const initMessage = {
|
|||||||
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
|
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The PTY worker always emits this final line. Treat it as a
|
||||||
|
// completion fallback because some WebSocket servers tear down the
|
||||||
|
// connection before the following structured message is flushed.
|
||||||
|
const exitMatch = typeof event.data === "string"
|
||||||
|
? event.data.match(/\[Script exited with code (-?\d+)\]/)
|
||||||
|
: null
|
||||||
|
if (exitMatch) {
|
||||||
|
const exitCode = Number(exitMatch[1])
|
||||||
|
termRef.current?.write(event.data)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
@@ -189,6 +219,15 @@ const initMessage = {
|
|||||||
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === "script_complete") {
|
||||||
|
const exitCode = Number(msg.exit_code ?? 1)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
termRef.current?.write(event.data)
|
termRef.current?.write(event.data)
|
||||||
setIsWaitingNextInteraction(false)
|
setIsWaitingNextInteraction(false)
|
||||||
@@ -199,6 +238,9 @@ const initMessage = {
|
|||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
|
if (!completionReceivedRef.current) {
|
||||||
|
termRef.current?.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
@@ -207,16 +249,19 @@ const initMessage = {
|
|||||||
clearInterval(keepAliveIntervalRef.current)
|
clearInterval(keepAliveIntervalRef.current)
|
||||||
keepAliveIntervalRef.current = null
|
keepAliveIntervalRef.current = null
|
||||||
}
|
}
|
||||||
|
if (completionReceivedRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!isComplete && reconnectAttemptsRef.current < 3) {
|
if (!isComplete && reconnectAttemptsRef.current < 3) {
|
||||||
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
|
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
|
||||||
} else {
|
} else {
|
||||||
setIsComplete(true)
|
setIsComplete(true)
|
||||||
onCompleteRef.current?.()
|
onCompleteRef.current?.(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}, [isOpen, isComplete, scriptPath])
|
}, [isOpen, isComplete, scriptPath, getCompletionMessage, t])
|
||||||
|
|
||||||
const sendKey = useCallback((key: string) => {
|
const sendKey = useCallback((key: string) => {
|
||||||
if (!termRef.current) return
|
if (!termRef.current) return
|
||||||
@@ -352,6 +397,23 @@ const initMessage = {
|
|||||||
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
|
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See the reconnect handler above. The exit line is guaranteed to be
|
||||||
|
// sent with the PTY output and is therefore a robust fallback when a
|
||||||
|
// final JSON frame is lost during server-side socket teardown.
|
||||||
|
const exitMatch = typeof event.data === "string"
|
||||||
|
? event.data.match(/\[Script exited with code (-?\d+)\]/)
|
||||||
|
: null
|
||||||
|
if (exitMatch) {
|
||||||
|
const exitCode = Number(exitMatch[1])
|
||||||
|
term.write(event.data)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
@@ -376,6 +438,15 @@ const initMessage = {
|
|||||||
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === "script_complete") {
|
||||||
|
const exitCode = Number(msg.exit_code ?? 1)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not JSON, es output normal de terminal
|
// Not JSON, es output normal de terminal
|
||||||
}
|
}
|
||||||
@@ -390,21 +461,25 @@ const initMessage = {
|
|||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
term.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
if (!completionReceivedRef.current) {
|
||||||
|
term.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
term.writeln(`\x1b[33m${t("scriptTerminal.connectionClosed")}\x1b[0m`)
|
if (!completionReceivedRef.current) {
|
||||||
|
term.writeln(`\x1b[33m${t("scriptTerminal.connectionClosed")}\x1b[0m`)
|
||||||
|
}
|
||||||
|
|
||||||
if (keepAliveIntervalRef.current) {
|
if (keepAliveIntervalRef.current) {
|
||||||
clearInterval(keepAliveIntervalRef.current)
|
clearInterval(keepAliveIntervalRef.current)
|
||||||
keepAliveIntervalRef.current = null
|
keepAliveIntervalRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isComplete) {
|
if (!completionReceivedRef.current && !isComplete) {
|
||||||
setIsComplete(true)
|
setIsComplete(true)
|
||||||
onCompleteRef.current?.()
|
onCompleteRef.current?.(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +566,7 @@ const initMessage = {
|
|||||||
|
|
||||||
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
|
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
|
||||||
reconnectAttemptsRef.current = 0
|
reconnectAttemptsRef.current = 0
|
||||||
|
completionReceivedRef.current = false
|
||||||
setIsComplete(false)
|
setIsComplete(false)
|
||||||
setInteractionInput("")
|
setInteractionInput("")
|
||||||
setCurrentInteraction(null)
|
setCurrentInteraction(null)
|
||||||
|
|||||||
@@ -1464,7 +1464,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
setApplyStartedAt(Date.now())
|
setApplyStartedAt(Date.now())
|
||||||
setApplyOpen(true)
|
setApplyOpen(true)
|
||||||
}
|
}
|
||||||
const handleApplyComplete = async () => {
|
const handleApplyComplete = async (exitCode = 0) => {
|
||||||
if (applyVmid == null) return
|
if (applyVmid == null) return
|
||||||
const duration = Math.max(0, Math.round((Date.now() - applyStartedAt) / 1000))
|
const duration = Math.max(0, Math.round((Date.now() - applyStartedAt) / 1000))
|
||||||
// The modal fires onComplete on any WS close (success or user cancel);
|
// The modal fires onComplete on any WS close (success or user cancel);
|
||||||
@@ -1477,7 +1477,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
success: true,
|
success: exitCode === 0,
|
||||||
target: applyTarget,
|
target: applyTarget,
|
||||||
duration_seconds: duration,
|
duration_seconds: duration,
|
||||||
ct_name: selectedVM?.name || `CT-${applyVmid}`,
|
ct_name: selectedVM?.name || `CT-${applyVmid}`,
|
||||||
@@ -2410,12 +2410,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
>
|
>
|
||||||
<Package className="h-4 w-4" />
|
<Package className="h-4 w-4" />
|
||||||
<span className={activeModalTab === "app" ? "" : "hidden sm:inline"}>
|
<span className={activeModalTab === "app" ? "" : "hidden sm:inline"}>
|
||||||
App
|
{t("vmLxc.tabs.app")}
|
||||||
</span>
|
</span>
|
||||||
{(selectedVM.app_watches || []).some(
|
{(selectedVM.app_watches || []).some(
|
||||||
(a) => a.update_available && !a.managed_oci_app_id,
|
(a) => a.update_available && !a.managed_oci_app_id,
|
||||||
) && (
|
) && (
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 ml-0.5" title="Update available" />
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 ml-0.5" title={t("vmLxc.appEditor.updateAvailableBadge")} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -3438,7 +3438,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
managedEntry
|
managedEntry
|
||||||
? {
|
? {
|
||||||
managed_oci_app_id: managedEntry.managed_oci_app_id!,
|
managed_oci_app_id: managedEntry.managed_oci_app_id!,
|
||||||
name: managedEntry.name || "Managed app",
|
name: managedEntry.name || t("vmLxc.appEditor.managedApp"),
|
||||||
installed_version: managedEntry.installed_version,
|
installed_version: managedEntry.installed_version,
|
||||||
latest_version: managedEntry.latest_version,
|
latest_version: managedEntry.latest_version,
|
||||||
update_available: managedEntry.update_available,
|
update_available: managedEntry.update_available,
|
||||||
@@ -3490,19 +3490,19 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
<Shield className="h-4 w-4 text-emerald-400" />
|
<Shield className="h-4 w-4 text-emerald-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
{aw?.name || "Managed app"}
|
{aw?.name || t("vmLxc.updates.managedApp")}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="text-xs bg-emerald-500/10 text-emerald-400 border-emerald-500/30">
|
<Badge variant="outline" className="text-xs bg-emerald-500/10 text-emerald-400 border-emerald-500/30">
|
||||||
managed
|
{t("vmLxc.updates.managedBadge")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{hasUpdate ? (
|
{hasUpdate ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Last checked: {lastChecked} ·{" "}
|
{t("vmLxc.updates.lastCheckedAt", { date: lastChecked })} ·{" "}
|
||||||
<span className="text-purple-400 font-medium">
|
<span className="text-purple-400 font-medium">
|
||||||
Tailscale v{aw?.latest_version} available
|
{t("vmLxc.updates.tailscaleAvailable", { version: aw?.latest_version || "" })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -3517,24 +3517,24 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
className="bg-purple-600/15 hover:bg-purple-600/25 border border-purple-500/40 text-purple-300 hover:text-purple-200"
|
className="bg-purple-600/15 hover:bg-purple-600/25 border border-purple-500/40 text-purple-300 hover:text-purple-200"
|
||||||
>
|
>
|
||||||
<ArrowUpCircle className="h-4 w-4 mr-1.5" />
|
<ArrowUpCircle className="h-4 w-4 mr-1.5" />
|
||||||
Update to v{aw?.latest_version}
|
{t("vmLxc.updates.updateToVersion", { version: aw?.latest_version || "" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{others > 0 && (
|
{others > 0 && (
|
||||||
<div className="text-[11px] text-muted-foreground">
|
<div className="text-[11px] text-muted-foreground">
|
||||||
+{others} other package{others === 1 ? "" : "s"} pending in the container
|
{t(others === 1 ? "vmLxc.updates.otherPackagePending" : "vmLxc.updates.otherPackagesPending", { count: others })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : upToDate ? (
|
) : upToDate ? (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Last checked: {lastChecked}
|
{t("vmLxc.updates.lastCheckedAt", { date: lastChecked })}
|
||||||
{aw?.installed_version && <> · Tailscale v{aw.installed_version}</>}
|
{aw?.installed_version && <> · Tailscale v{aw.installed_version}</>}
|
||||||
{" · "}<span className="text-emerald-400/90">{t("vmLxc.updates.noUpdatesAvailableChip")}</span>
|
{" · "}<span className="text-emerald-400/90">{t("vmLxc.updates.noUpdatesAvailableChip")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
No update information yet — check from Security → Secure Gateway.
|
{t("vmLxc.updates.noManagedUpdateInfo")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -3552,15 +3552,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
<Container className="h-4 w-4 text-blue-400" />
|
<Container className="h-4 w-4 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
OCI image container
|
{t("vmLxc.updates.ociTitle")}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
This container was created from an OCI (Docker) image.
|
{t("vmLxc.updates.ociBody")}
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -3777,7 +3773,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
|
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
|
||||||
<span>upstream <code className="text-purple-300">{matchApp!.latest_version}</code> available</span>
|
<span>{t("vmLxc.updates.upstreamAvailable", { version: matchApp!.latest_version || "" })}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : upToD ? (
|
) : upToD ? (
|
||||||
@@ -3819,16 +3815,16 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
{!helperExists && (
|
{!helperExists && (
|
||||||
helperKnownNotUpdateable ? (
|
helperKnownNotUpdateable ? (
|
||||||
<p className="text-xs text-amber-400/90 leading-relaxed">
|
<p className="text-xs text-amber-400/90 leading-relaxed">
|
||||||
The community-scripts registry marks this app as not updateable
|
{t("vmLxc.updates.helperNotUpdateable")}
|
||||||
in place. Apply updates manually or reinstall with a newer
|
in place. Apply updates manually or reinstall with a newer
|
||||||
helper-script.
|
helper-script.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
Detected a helper-scripts updater
|
{t("vmLxc.updates.helperDetectedTitle")}
|
||||||
(<code className="text-foreground/80">{uc?.helper_slug}</code>)
|
(<code className="text-foreground/80">{uc?.helper_slug}</code>)
|
||||||
but it isn't listed in the ProxMenux helpers catalogue.
|
but it isn't listed in the ProxMenux helpers catalogue.
|
||||||
Applying manually is safest.
|
{t("vmLxc.updates.helperDetectedBody")}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -3922,7 +3918,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
|
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
|
||||||
<span>upstream <code className="text-purple-300">{aw.latest_version}</code> available</span>
|
<span>{t("vmLxc.updates.upstreamAvailable", { version: aw.latest_version || "" })}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : upToDate ? (
|
) : upToDate ? (
|
||||||
@@ -4103,7 +4099,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{applyBackup
|
{applyBackup
|
||||||
? <>{t("vmLxc.options.snapshotBefore")} <span className="text-muted-foreground">— on <code className="text-foreground/80">{applyBackupStorage || selectedBackupStorage || t("vmLxc.options.storageAuto")}</code></span></>
|
? <>{t("vmLxc.options.snapshotBefore")} <span className="text-muted-foreground">{t("vmLxc.options.snapshotOn", { storage: applyBackupStorage || selectedBackupStorage || t("vmLxc.options.storageAuto") })}</span></>
|
||||||
: <span className="text-muted-foreground">{t("vmLxc.options.noSnapshot")}</span>}
|
: <span className="text-muted-foreground">{t("vmLxc.options.noSnapshot")}</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -4971,13 +4967,17 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
onComplete={handleApplyComplete}
|
onComplete={handleApplyComplete}
|
||||||
scriptPath="/usr/local/share/proxmenux/scripts/lxc/apply_updates.sh"
|
scriptPath="/usr/local/share/proxmenux/scripts/lxc/apply_updates.sh"
|
||||||
scriptName="lxc_apply_updates"
|
scriptName="lxc_apply_updates"
|
||||||
title={`Apply updates — CT ${applyVmid}`}
|
title={t("vmLxc.updates.terminalTitle", { vmid: applyVmid })}
|
||||||
|
completedSuccessfullyMessage={t("vmLxc.updates.terminalCompletedSuccessfully")}
|
||||||
|
completedWithErrorMessage={(exitCode) =>
|
||||||
|
t("vmLxc.updates.terminalCompletedWithError", { code: exitCode })
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
applyTarget === "os"
|
applyTarget === "os"
|
||||||
? "Applying OS package updates inside the container..."
|
? t("vmLxc.updates.terminalDescriptionOs")
|
||||||
: applyTarget === "app"
|
: applyTarget === "app"
|
||||||
? "Running the application updater inside the container..."
|
? t("vmLxc.updates.terminalDescriptionApp")
|
||||||
: "Applying OS package + application updates inside the container..."
|
: t("vmLxc.updates.terminalDescriptionBoth")
|
||||||
}
|
}
|
||||||
params={{
|
params={{
|
||||||
VMID: String(applyVmid),
|
VMID: String(applyVmid),
|
||||||
|
|||||||
@@ -1201,6 +1201,12 @@
|
|||||||
"applyUpdate": "Apply update",
|
"applyUpdate": "Apply update",
|
||||||
"upToDate": "Up to date",
|
"upToDate": "Up to date",
|
||||||
"runUpdater": "Run updater",
|
"runUpdater": "Run updater",
|
||||||
|
"terminalTitle": "Apply updates — CT {vmid}",
|
||||||
|
"terminalDescriptionOs": "Applying OS package updates inside the container...",
|
||||||
|
"terminalDescriptionApp": "Running the application updater inside the container...",
|
||||||
|
"terminalDescriptionBoth": "Applying OS package and application updates inside the container...",
|
||||||
|
"terminalCompletedSuccessfully": "Updates completed successfully",
|
||||||
|
"terminalCompletedWithError": "Updates finished with error code {code}",
|
||||||
"applyOsPlusApp": "Apply OS + {appName} updates",
|
"applyOsPlusApp": "Apply OS + {appName} updates",
|
||||||
"applyOsPlusApps": "Apply OS + Apps updates",
|
"applyOsPlusApps": "Apply OS + Apps updates",
|
||||||
"osPlusApp": "OS + {appName} updates",
|
"osPlusApp": "OS + {appName} updates",
|
||||||
@@ -1244,7 +1250,20 @@
|
|||||||
"applyOsAppsFooterPlural": "Apply OS + Apps updates",
|
"applyOsAppsFooterPlural": "Apply OS + Apps updates",
|
||||||
"osAppsFooter": "OS + {appName} updates",
|
"osAppsFooter": "OS + {appName} updates",
|
||||||
"osAppsFooterPlural": "OS + Apps updates",
|
"osAppsFooterPlural": "OS + Apps updates",
|
||||||
"noUpdatesAvailableChip": "No updates available"
|
"noUpdatesAvailableChip": "No updates available",
|
||||||
|
"managedApp": "Managed app",
|
||||||
|
"managedBadge": "managed",
|
||||||
|
"lastCheckedAt": "Last checked: {date}",
|
||||||
|
"tailscaleAvailable": "Tailscale v{version} available",
|
||||||
|
"updateToVersion": "Update to v{version}",
|
||||||
|
"otherPackagePending": "+{count} other package pending in the container",
|
||||||
|
"otherPackagesPending": "+{count} other packages pending in the container",
|
||||||
|
"noManagedUpdateInfo": "No update information yet — check from Security → Secure Gateway.",
|
||||||
|
"ociTitle": "OCI image container",
|
||||||
|
"ociBody": "This container was created from an OCI (Docker) image. Update management for OCI containers is coming with the upcoming OCI install feature — updates will rebuild the container from a newer image tag rather than patching packages inside.",
|
||||||
|
"helperNotUpdateable": "The community-scripts registry marks this app as not updateable.",
|
||||||
|
"helperDetectedTitle": "Detected a helper-scripts updater",
|
||||||
|
"helperDetectedBody": "Applying manually is safest."
|
||||||
},
|
},
|
||||||
"appEditor": {
|
"appEditor": {
|
||||||
"closePanel": "Close panel",
|
"closePanel": "Close panel",
|
||||||
@@ -1351,7 +1370,45 @@
|
|||||||
"versionRegexOptional": "Version regex (optional)",
|
"versionRegexOptional": "Version regex (optional)",
|
||||||
"top20Matches": "Top 20 matches — narrow the query for more",
|
"top20Matches": "Top 20 matches — narrow the query for more",
|
||||||
"binaryArgsHintPrefix": "Left empty defaults to",
|
"binaryArgsHintPrefix": "Left empty defaults to",
|
||||||
"binaryArgsHintGrafana": "Grafana needs"
|
"binaryArgsHintGrafana": "Grafana needs",
|
||||||
|
"loadFailed": "Could not load app configuration",
|
||||||
|
"checkFailed": "Check failed",
|
||||||
|
"deleteFailed": "Delete failed",
|
||||||
|
"dismissFailed": "Could not dismiss detection",
|
||||||
|
"restoreFailed": "Could not restore detection",
|
||||||
|
"managedApp": "Managed app",
|
||||||
|
"managedStatus": "managed",
|
||||||
|
"checkedAt": "Checked {date}",
|
||||||
|
"latestUpstream": "Latest upstream",
|
||||||
|
"loadingApplications": "Loading applications…",
|
||||||
|
"nameSearchPlaceholder": "Type to search 700+ apps, or type your own",
|
||||||
|
"matchCount": "{count} matches",
|
||||||
|
"catalogPort": "port {port}",
|
||||||
|
"trackingAvailable": "tracking available",
|
||||||
|
"logoUrlLabel": "Logo URL",
|
||||||
|
"webLinks": "Web links",
|
||||||
|
"addPort": "Add port",
|
||||||
|
"detectedPorts": "Ports detected in the container — click to add:",
|
||||||
|
"noWebPorts": "No web ports listening. Use Add port if you still want to create a link manually.",
|
||||||
|
"trackUpstream": "Track upstream version (optional)",
|
||||||
|
"trackOff": "Off — link only",
|
||||||
|
"trackOn": "On · {method}",
|
||||||
|
"trackHelp": "Optional. When enabled, ProxMenux checks the installed version and compares it with the latest upstream release.",
|
||||||
|
"commandSafetyHelp": "Runs argv-style inside the CT as root — never through a shell. You are responsible for what the command does. Use installed_regex below to extract the version from the output.",
|
||||||
|
"manualVersionHelp": "Type the version you have installed. If an upstream source is set below, ProxMenux still checks for updates. After upgrading the app, return here and update this value.",
|
||||||
|
"upstreamHelp": "How ProxMenux finds the latest version to compare against your installed one.",
|
||||||
|
"githubRepoHelp": "Public GitHub repository where releases or tags are published.",
|
||||||
|
"httpJsonHelp": "Public JSON endpoint that returns a version somewhere in the payload.",
|
||||||
|
"jsonPathHelp": "Dotted path with optional [N] array indices.",
|
||||||
|
"restoreButton": "Restore",
|
||||||
|
"registerButton": "Register",
|
||||||
|
"removeButton": "Remove",
|
||||||
|
"checkButton": "Check",
|
||||||
|
"editFieldsButton": "Edit fields",
|
||||||
|
"alsoDetectedContainer": "Also detected on this container",
|
||||||
|
"addAnotherApplication": "Add another application",
|
||||||
|
"doneButton": "Done",
|
||||||
|
"editButton": "Edit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -4598,6 +4655,8 @@
|
|||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"websocketError": "WebSocket error occurred",
|
"websocketError": "WebSocket error occurred",
|
||||||
"connectionClosed": "Connection closed",
|
"connectionClosed": "Connection closed",
|
||||||
|
"completedSuccessfully": "Script completed successfully",
|
||||||
|
"completedWithError": "Script finished with error code {code}",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"yourInput": "Your input:",
|
"yourInput": "Your input:",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
|||||||
@@ -1034,7 +1034,8 @@
|
|||||||
"mounts": "Pripojenia",
|
"mounts": "Pripojenia",
|
||||||
"backups": "Zálohy",
|
"backups": "Zálohy",
|
||||||
"updates": "Aktualizácie",
|
"updates": "Aktualizácie",
|
||||||
"firewall": "Firewall"
|
"firewall": "Firewall",
|
||||||
|
"app": "Aplikácie"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"start": "Spustiť",
|
"start": "Spustiť",
|
||||||
@@ -1128,6 +1129,286 @@
|
|||||||
"readFailed": "Firewall log sa nepodarilo načítať",
|
"readFailed": "Firewall log sa nepodarilo načítať",
|
||||||
"empty": "Zatiaľ tu nie sú žiadne záznamy.",
|
"empty": "Zatiaľ tu nie sú žiadne záznamy.",
|
||||||
"emptyHint": "Pravidlá s log: info alebo vyššie začnú tento prehľad napĺňať, keď príde sieťová prevádzka."
|
"emptyHint": "Pravidlá s log: info alebo vyššie začnú tento prehľad napĺňať, keď príde sieťová prevádzka."
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Možnosti",
|
||||||
|
"snapshotBefore": "Snapshot pred použitím zmien",
|
||||||
|
"snapshotOn": "— na úložisku {storage}",
|
||||||
|
"noSnapshot": "Bez snapshotu pred použitím zmien",
|
||||||
|
"restartAfter": "Po použití zmien reštartovať kontajner",
|
||||||
|
"noRestart": "Bez reštartu po použití zmien",
|
||||||
|
"snapshotLabel": "Pred použitím zmien vytvoriť snapshot kontajnera (vzdump)",
|
||||||
|
"appliesToBoth": "Platí pre ručné aj plánované spustenie.",
|
||||||
|
"backupStorage": "Úložisko zálohy",
|
||||||
|
"pickStorage": "Vyberte úložisko",
|
||||||
|
"editButton": "Upraviť",
|
||||||
|
"cancelButton": "Zrušiť",
|
||||||
|
"saveButton": "Uložiť",
|
||||||
|
"storageAuto": "automaticky"
|
||||||
|
},
|
||||||
|
"scheduled": {
|
||||||
|
"title": "Plánované aktualizácie",
|
||||||
|
"enabledHelper": "Prebehnú na pozadí s použitím možností uvedených vyššie.",
|
||||||
|
"disabledHelper": "Zapnite automatické aktualizácie podľa cron plánu.",
|
||||||
|
"chipLabel": "Plán",
|
||||||
|
"disabledSuffix": "(vypnuté)",
|
||||||
|
"whatLabel": "Čo:",
|
||||||
|
"targetOs": "Balíky systému",
|
||||||
|
"targetApp": "Aplikácia",
|
||||||
|
"targetBoth": "Systém + aplikácia",
|
||||||
|
"lastRun": "Posledné spustenie: {date}",
|
||||||
|
"runSuccess": "✓ úspešné",
|
||||||
|
"runFailed": "✗ zlyhalo",
|
||||||
|
"notScheduled": "Bez plánu",
|
||||||
|
"frequency": "Frekvencia",
|
||||||
|
"cronExpression": "Cron výraz",
|
||||||
|
"cronPlaceholder": "napr. 0 3 * * *",
|
||||||
|
"whatToUpdate": "Čo aktualizovať",
|
||||||
|
"targetOptionOs": "Iba balíky systému",
|
||||||
|
"targetOptionApp": "Iba aplikáciu",
|
||||||
|
"targetOptionBoth": "Systém + aplikáciu",
|
||||||
|
"deleteButton": "Odstrániť plán",
|
||||||
|
"deleteConfirm": "Odstrániť plánované aktualizácie tohto kontajnera? Predvolené nastavenia (záloha a reštart) zostanú zachované."
|
||||||
|
},
|
||||||
|
"cronChip": {
|
||||||
|
"detected": "zistený cron na serveri",
|
||||||
|
"variantTteck": "tteck (pôvodné)",
|
||||||
|
"variantCustom": "vlastné",
|
||||||
|
"variantCommunityScripts": "community-scripts",
|
||||||
|
"scopeAptApk": "apt/apk vo všetkých CT",
|
||||||
|
"scopeUnknown": "neznámy rozsah"
|
||||||
|
},
|
||||||
|
"cronPresets": {
|
||||||
|
"hourly": "Každú hodinu",
|
||||||
|
"dailyAt3": "Denne o 3:00",
|
||||||
|
"dailyAtNoon": "Denne o 12:00",
|
||||||
|
"weeklySun3": "Týždenne (v nedeľu o 3:00)",
|
||||||
|
"monthly1st3": "Mesačne (1. deň o 3:00)",
|
||||||
|
"custom": "Vlastný cron…"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"osPackagesTitle": "Balíky systému",
|
||||||
|
"lastCheckedPrefix": "Naposledy skontrolované:",
|
||||||
|
"familyLabel": "Systém:",
|
||||||
|
"applyOsUpdate": "Aktualizovať systém",
|
||||||
|
"osUpToDate": "Systém je aktuálny",
|
||||||
|
"installedByHelperPrefix": "Nainštalované cez",
|
||||||
|
"helperScriptsName": "Proxmox Helper-Scripts",
|
||||||
|
"helperUpdatesRun": "— aktualizácie spustia pomocný skript z community-scripts.",
|
||||||
|
"installedPrefix": "nainštalovaná verzia",
|
||||||
|
"upstreamAvailable": "dostupná verzia {version}",
|
||||||
|
"upToDateAt": "Aktuálna verzia",
|
||||||
|
"applyUpdate": "Aktualizovať",
|
||||||
|
"upToDate": "Aktuálne",
|
||||||
|
"runUpdater": "Spustiť aktualizáciu",
|
||||||
|
"terminalTitle": "Aktualizácia — CT {vmid}",
|
||||||
|
"terminalDescriptionOs": "Aktualizujem balíky systému v kontajneri...",
|
||||||
|
"terminalDescriptionApp": "Spúšťam aktualizáciu aplikácie v kontajneri...",
|
||||||
|
"terminalDescriptionBoth": "Aktualizujem systém aj aplikácie v kontajneri...",
|
||||||
|
"terminalCompletedSuccessfully": "Aktualizácia bola úspešne dokončená",
|
||||||
|
"terminalCompletedWithError": "Aktualizácia skončila s chybovým kódom {code}",
|
||||||
|
"applyOsPlusApp": "Aktualizovať systém + {appName}",
|
||||||
|
"applyOsPlusApps": "Aktualizovať systém + aplikácie",
|
||||||
|
"osPlusApp": "Aktualizácie systému + {appName}",
|
||||||
|
"osPlusApps": "Aktualizácie systému + aplikácií",
|
||||||
|
"noUpdateMethodTitle": "Aktualizácia nie je nastavená",
|
||||||
|
"noUpdateMethodBody": "Pre túto aplikáciu nie je nastavený spôsob aktualizácie. Aktualizujte ju priamo v aplikácii alebo nastavte vlastný príkaz na karte Aplikácia.",
|
||||||
|
"ociImmutableTitle": "Kontajner z OCI obrazu",
|
||||||
|
"ociImmutableBody": "Balíky systému sú súčasťou obrazu a nemožno ich aktualizovať priamo. Aktualizujte ich ručne alebo kontajner preinštalujte z novšieho obrazu.",
|
||||||
|
"hideNoticeButton": "Skryť toto upozornenie pre aplikáciu",
|
||||||
|
"noOsUpdatesPending": "Systém nečaká na žiadne aktualizácie",
|
||||||
|
"securityUpdateAria": "Bezpečnostná aktualizácia",
|
||||||
|
"packagePendingLabel": "čakajúci balík",
|
||||||
|
"packagesPendingLabel": "čakajúcich balíkov",
|
||||||
|
"securityUpdateLabel": "bezpečnostná aktualizácia",
|
||||||
|
"securityUpdatesLabel": "bezpečnostných aktualizácií",
|
||||||
|
"applicationDefaultName": "Aplikácia",
|
||||||
|
"installedLabel": "nainštalované",
|
||||||
|
"upToDateAtLabel": "Aktuálna verzia",
|
||||||
|
"versionTrackingPending": "Čaká sa na údaje o verzii — nainštalovaná a dostupná verzia sa doplnia pri najbližšej plánovanej kontrole.",
|
||||||
|
"customCommandTitle": "Vlastný príkaz na aktualizáciu",
|
||||||
|
"customCommandBody": "Spustí v kontajneri vlastný shell príkaz, ktorý aktualizuje túto aplikáciu.",
|
||||||
|
"noUpdaterConfiguredTitle": "Aktualizácia nie je nastavená",
|
||||||
|
"noUpdaterConfiguredBody": "Táto aplikácia nemá pomocný inštalátor ani vlastný príkaz na aktualizáciu.",
|
||||||
|
"hideNoticeAction": "Skryť upozornenie pre {appName}",
|
||||||
|
"detectedByPrefix": "Zistené cez",
|
||||||
|
"editApp": "Upraviť",
|
||||||
|
"alsoDetectedTitle": "V tomto kontajneri sa našli aj:",
|
||||||
|
"detectedInline": "· zistené cez {method}",
|
||||||
|
"customCommandLabel": "Vlastný príkaz na aktualizáciu",
|
||||||
|
"customCommandPlaceholder": "napr. cd /opt/<your-app> && git pull && systemctl restart <your-app>",
|
||||||
|
"removeButton": "Odstrániť",
|
||||||
|
"cancelButton": "Zrušiť",
|
||||||
|
"saveButton": "Uložiť",
|
||||||
|
"editCommandButton": "Upraviť príkaz",
|
||||||
|
"wireUpCommandButton": "Pridať vlastný príkaz na aktualizáciu",
|
||||||
|
"versionTrackingPendingShort": "Čaká sa na údaje o verzii.",
|
||||||
|
"noMethodBody": "Pre túto aplikáciu nie je nastavený spôsob aktualizácie. Aktualizujte ju priamo v aplikácii alebo pridajte vlastný príkaz, ktorý ProxMenux spustí v kontajneri.",
|
||||||
|
"noMethodHideBody": "Táto aplikácia nemá nastavený spôsob aktualizácie. Nastavte ho na karte Aplikácia alebo toto upozornenie skryte.",
|
||||||
|
"hideForApp": "Skryť upozornenie pre {appName}",
|
||||||
|
"applyOsAppsFooter": "Aktualizovať systém + {appName}",
|
||||||
|
"applyOsAppsFooterPlural": "Aktualizovať systém + aplikácie",
|
||||||
|
"osAppsFooter": "Aktualizácie systému + {appName}",
|
||||||
|
"osAppsFooterPlural": "Aktualizácie systému + aplikácií",
|
||||||
|
"noUpdatesAvailableChip": "Nie sú dostupné žiadne aktualizácie",
|
||||||
|
"managedApp": "Spravovaná aplikácia",
|
||||||
|
"managedBadge": "spravované",
|
||||||
|
"lastCheckedAt": "Naposledy skontrolované: {date}",
|
||||||
|
"tailscaleAvailable": "Dostupný Tailscale v{version}",
|
||||||
|
"updateToVersion": "Aktualizovať na v{version}",
|
||||||
|
"otherPackagePending": "+{count} ďalší čakajúci balík v kontajneri",
|
||||||
|
"otherPackagesPending": "+{count} ďalších čakajúcich balíkov v kontajneri",
|
||||||
|
"noManagedUpdateInfo": "Zatiaľ nie sú dostupné informácie o aktualizácii — skontrolujte ich v časti Bezpečnosť → Bezpečná brána.",
|
||||||
|
"ociTitle": "Kontajner z OCI obrazu",
|
||||||
|
"ociBody": "Tento kontajner bol vytvorený z OCI (Docker) obrazu. Pripravovaná podpora aktualizácií OCI kontajnerov vytvorí kontajner znova z novšieho tagu obrazu namiesto aktualizácie balíkov vo vnútri.",
|
||||||
|
"helperNotUpdateable": "Register community-scripts označuje túto aplikáciu ako neaktualizovateľnú.",
|
||||||
|
"helperDetectedTitle": "Našiel sa aktualizačný skript z helper-scripts",
|
||||||
|
"helperDetectedBody": "Najbezpečnejšie je spustiť aktualizáciu ručne."
|
||||||
|
},
|
||||||
|
"appEditor": {
|
||||||
|
"closePanel": "Zavrieť panel",
|
||||||
|
"cancelButton": "Zrušiť",
|
||||||
|
"saveButton": "Uložiť",
|
||||||
|
"hideButton": "Skryť",
|
||||||
|
"hidePermanentlyTooltip": "Natrvalo skryť toto zistenie",
|
||||||
|
"hiddenBadge": "Momentálne skryté — po obnovení sa znova zobrazí v zozname nájdených aplikácií",
|
||||||
|
"registerDifferent": "Pridať inú aplikáciu",
|
||||||
|
"detectedInContainer": "Nájdené v tomto kontajneri",
|
||||||
|
"installedManaged": "Nainštalované a spravované cez ProxMenux",
|
||||||
|
"nameLabel": "Názov",
|
||||||
|
"installedViaLabel": "Spôsob inštalácie",
|
||||||
|
"installedVersionLabel": "Nainštalovaná verzia",
|
||||||
|
"installedVersionRegexLabel": "Regex nainštalovanej verzie (zachytávacia skupina)",
|
||||||
|
"installedRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
|
||||||
|
"installedRegexAlt": "installed_regex",
|
||||||
|
"regexCaptureGroupLabel": "Regex (so zachytávacou skupinou)",
|
||||||
|
"regexPlaceholderVersion": "version:\\s*(\\d+\\.\\d+\\.\\d+)",
|
||||||
|
"optionalSuffix": "(voliteľné)",
|
||||||
|
"methodNone": "Žiadny (iba odkaz)",
|
||||||
|
"methodDpkg": "balík dpkg (Debian/Ubuntu)",
|
||||||
|
"methodApk": "balík apk (Alpine)",
|
||||||
|
"methodBinary": "Binárny súbor (úplná cesta alebo názov)",
|
||||||
|
"methodFile": "súbor + regex",
|
||||||
|
"methodDockerLabel": "docker inspect (OCI label)",
|
||||||
|
"methodDockerExec": "docker exec (binárny súbor v kontajneri)",
|
||||||
|
"methodPython": "Python distribúcia (importlib.metadata)",
|
||||||
|
"methodCommand": "príkaz (pokročilé — argv)",
|
||||||
|
"methodManual": "ručne (verziu zadávam sám)",
|
||||||
|
"packageIdentifierLabel": "Identifikátor balíka",
|
||||||
|
"binaryPathLabel": "Cesta k binárnemu súboru",
|
||||||
|
"binaryPathPlaceholder": "napr. /opt/<your-app>/binary",
|
||||||
|
"binaryPathBare": "Binárny súbor (úplná cesta alebo názov)",
|
||||||
|
"binaryPathBarePlaceholder": "napr. myapp (alebo úplná cesta)",
|
||||||
|
"binaryArgsLabel": "Argumenty binárneho súboru (oddelené čiarkou, voliteľné)",
|
||||||
|
"binaryArgsPlaceholder": "napr. --version (oddelené čiarkou)",
|
||||||
|
"filePathLabel": "Cesta k súboru",
|
||||||
|
"commandLabel": "Príkaz (argv oddelené čiarkou, bez shellu)",
|
||||||
|
"commandPlaceholder": "myapp, version, --json",
|
||||||
|
"commandServerExample": "server, -v",
|
||||||
|
"containerNameLabel": "Názov kontajnera",
|
||||||
|
"containerNamePlaceholder": "napr. <your-container-name>",
|
||||||
|
"ociLabelKeyLabel": "Kľúč OCI labelu",
|
||||||
|
"ociLabelPlaceholder": "org.opencontainers.image.version",
|
||||||
|
"pythonInterpreterLabel": "Cesta k Python interpreteru",
|
||||||
|
"pythonInterpreterPlaceholder": "napr. /opt/<your-app>/venv/bin/python",
|
||||||
|
"pipDistLabel": "Názov distribúcie (pip balík)",
|
||||||
|
"pipDistPlaceholder": "napr. <your-pip-package>",
|
||||||
|
"upstreamSourceLabel": "Zdroj dostupnej verzie (voliteľné)",
|
||||||
|
"upstreamNone": "žiadny (nekontrolovať dostupnú verziu)",
|
||||||
|
"upstreamGithub": "GitHub repozitár",
|
||||||
|
"upstreamHttp": "HTTP JSON endpoint",
|
||||||
|
"upstreamDocker": "Obraz na Docker Hub",
|
||||||
|
"githubRepoLabel": "GitHub repozitár",
|
||||||
|
"githubRepoPlaceholder": "napr. vlastník/názov",
|
||||||
|
"githubRepoAlt": "vlastník/názov",
|
||||||
|
"githubSourceLabel": "Zdroj na GitHube",
|
||||||
|
"sourceReleases": "vydania",
|
||||||
|
"sourceTags": "tagy",
|
||||||
|
"endpointUrlLabel": "URL endpointu",
|
||||||
|
"endpointUrlPlaceholder": "napr. https://vendor.example.com/api/latest.json",
|
||||||
|
"jsonPathLabel": "JSON cesta",
|
||||||
|
"jsonPathPlaceholder": "napr. data.version alebo releases[0].tag_name",
|
||||||
|
"dockerImageLabel": "Obraz na Docker Hub",
|
||||||
|
"dockerImagePlaceholder": "napr. linuxserver/plex alebo nginx",
|
||||||
|
"tagRegexLabel": "Regex tagu (so zachytávacou skupinou)",
|
||||||
|
"tagRegexPlaceholder": "napr. v?(\\d+\\.\\d+\\.\\d+)",
|
||||||
|
"tagRegexBare": "v?(\\d+\\.\\d+\\.\\d+)",
|
||||||
|
"installedStatus": "Nainštalované",
|
||||||
|
"checkingStatus": "kontrolujem…",
|
||||||
|
"latestFromLabel": "Najnovšia verzia z",
|
||||||
|
"upToDateBadge": "Aktuálne",
|
||||||
|
"updateAvailableBadge": "Dostupná aktualizácia",
|
||||||
|
"portDescriptionPlaceholder": "Popis (napr. Web UI, go2rtc, admin)",
|
||||||
|
"portPortPlaceholder": "port",
|
||||||
|
"portHttp": "http",
|
||||||
|
"portHttps": "https",
|
||||||
|
"portLogoLabel": "URL loga pre tento odkaz (voliteľné)",
|
||||||
|
"portLogoPlaceholder": "napr. https://example.com/logo.webp",
|
||||||
|
"removePortTooltip": "Odstrániť port",
|
||||||
|
"detectMethodDpkg": "dpkg ·",
|
||||||
|
"detectMethodApk": "apk ·",
|
||||||
|
"detectMethodBinary": "binárny súbor ·",
|
||||||
|
"detectMethodFile": "súbor ·",
|
||||||
|
"detectMethodDockerLabel": "docker label",
|
||||||
|
"detectMethodDockerExec": "docker exec",
|
||||||
|
"detectMethodCommand": "príkaz",
|
||||||
|
"binaryVersionHint": "binary --version",
|
||||||
|
"systemctlHint": "systemctl show <service> -p ExecStart",
|
||||||
|
"whichHint": "which <app>",
|
||||||
|
"apkInfoHint": "\" : \"apk info | grep",
|
||||||
|
"grafanaNote": ". Grafana potrebuje",
|
||||||
|
"hiddenDetectionsHelpSingular": "Máte {count} skrytú nájdenú aplikáciu. Obnovte ju alebo pridajte vlastnú aplikáciu ručne.",
|
||||||
|
"hiddenDetectionsHelpPlural": "Máte {count} skrytých nájdených aplikácií. Obnovte niektorú z nich alebo pridajte vlastnú aplikáciu ručne.",
|
||||||
|
"registerCustom": "Pridať vlastnú aplikáciu",
|
||||||
|
"noAppsTitle": "Nie sú pridané žiadne aplikácie",
|
||||||
|
"noAppsBody": "Pridajte aplikácie, ktoré bežia v tomto kontajneri. Získate odkazy na ich webové rozhranie a voliteľne aj sledovanie dostupných verzií s upozorneniami na nové vydania.",
|
||||||
|
"registerApplication": "Pridať aplikáciu",
|
||||||
|
"hiddenSuffix": "skryté: {count}",
|
||||||
|
"confirmHide": "Natrvalo skryť nájdenú aplikáciu „{name}“ v tomto kontajneri?\n\nAk ju budete chcieť neskôr vrátiť, kliknite na „Pridať inú aplikáciu“. V zozname skrytých aplikácií ju potom môžete obnoviť.",
|
||||||
|
"saveFailed": "Uloženie zlyhalo",
|
||||||
|
"tagFilterRegexOptional": "Regex na filtrovanie tagov (voliteľné)",
|
||||||
|
"versionRegexOptional": "Regex verzie (voliteľné)",
|
||||||
|
"top20Matches": "Prvých 20 výsledkov — spresnite hľadanie, ak chcete nájsť ďalšie",
|
||||||
|
"binaryArgsHintPrefix": "Ak pole necháte prázdne, použije sa",
|
||||||
|
"binaryArgsHintGrafana": "Grafana potrebuje",
|
||||||
|
"loadFailed": "Nastavenia aplikácií sa nepodarilo načítať",
|
||||||
|
"checkFailed": "Kontrola zlyhala",
|
||||||
|
"deleteFailed": "Odstránenie zlyhalo",
|
||||||
|
"dismissFailed": "Nájdenú aplikáciu sa nepodarilo skryť",
|
||||||
|
"restoreFailed": "Nájdenú aplikáciu sa nepodarilo obnoviť",
|
||||||
|
"managedApp": "Spravovaná aplikácia",
|
||||||
|
"managedStatus": "spravované",
|
||||||
|
"checkedAt": "Skontrolované {date}",
|
||||||
|
"latestUpstream": "Najnovšia dostupná",
|
||||||
|
"loadingApplications": "Načítavam aplikácie…",
|
||||||
|
"nameSearchPlaceholder": "Vyhľadajte medzi viac ako 700 aplikáciami alebo zadajte vlastný názov",
|
||||||
|
"matchCount": "Počet výsledkov: {count}",
|
||||||
|
"catalogPort": "port {port}",
|
||||||
|
"trackingAvailable": "možno sledovať verziu",
|
||||||
|
"logoUrlLabel": "URL loga",
|
||||||
|
"webLinks": "Webové odkazy",
|
||||||
|
"addPort": "Pridať port",
|
||||||
|
"detectedPorts": "Porty nájdené v kontajneri — kliknutím ich pridáte:",
|
||||||
|
"noWebPorts": "Nenašli sa žiadne aktívne webové porty. Odkaz môžete vytvoriť ručne cez Pridať port.",
|
||||||
|
"trackUpstream": "Sledovať dostupnú verziu (voliteľné)",
|
||||||
|
"trackOff": "Vypnuté — iba odkaz",
|
||||||
|
"trackOn": "Zapnuté · {method}",
|
||||||
|
"trackHelp": "Ak túto možnosť zapnete, ProxMenux porovná nainštalovanú verziu s najnovšou dostupnou verziou.",
|
||||||
|
"commandSafetyHelp": "Príkaz sa spustí priamo cez argv v CT ako root, nikdy nie cez shell. Za jeho obsah zodpovedáte vy. Verziu z výstupu získate pomocou installed_regex nižšie.",
|
||||||
|
"manualVersionHelp": "Zadajte nainštalovanú verziu. Ak nižšie nastavíte zdroj dostupnej verzie, ProxMenux bude ďalej kontrolovať aktualizácie. Po aktualizácii aplikácie sa sem vráťte a hodnotu upravte.",
|
||||||
|
"upstreamHelp": "Určuje, kde ProxMenux nájde najnovšiu verziu na porovnanie s nainštalovanou verziou.",
|
||||||
|
"githubRepoHelp": "Verejný GitHub repozitár, v ktorom sa zverejňujú vydania alebo tagy.",
|
||||||
|
"httpJsonHelp": "Verejný JSON endpoint, ktorý obsahuje číslo verzie.",
|
||||||
|
"jsonPathHelp": "Cesta oddelená bodkami s voliteľnými indexmi poľa [N].",
|
||||||
|
"restoreButton": "Obnoviť",
|
||||||
|
"registerButton": "Pridať",
|
||||||
|
"removeButton": "Odstrániť",
|
||||||
|
"checkButton": "Skontrolovať",
|
||||||
|
"editFieldsButton": "Upraviť údaje",
|
||||||
|
"alsoDetectedContainer": "Ďalšie aplikácie nájdené v kontajneri",
|
||||||
|
"addAnotherApplication": "Pridať ďalšiu aplikáciu",
|
||||||
|
"doneButton": "Hotovo",
|
||||||
|
"editButton": "Upraviť"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -3880,6 +4161,8 @@
|
|||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"websocketError": "Chyba WebSocket spojenia",
|
"websocketError": "Chyba WebSocket spojenia",
|
||||||
"connectionClosed": "Spojenie bolo zatvorené",
|
"connectionClosed": "Spojenie bolo zatvorené",
|
||||||
|
"completedSuccessfully": "Skript sa úspešne dokončil",
|
||||||
|
"completedWithError": "Skript skončil s chybovým kódom {code}",
|
||||||
"yes": "Áno",
|
"yes": "Áno",
|
||||||
"yourInput": "Tvoj vstup:",
|
"yourInput": "Tvoj vstup:",
|
||||||
"submit": "Odoslať",
|
"submit": "Odoslať",
|
||||||
|
|||||||
@@ -478,6 +478,15 @@ def script_websocket(ws, session_id):
|
|||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
env=env
|
env=env
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The child inherited the slave side of the PTY. Keeping the parent's
|
||||||
|
# duplicate open can prevent the reader from seeing EOF after the script
|
||||||
|
# exits, which in turn hides the final script_complete message.
|
||||||
|
try:
|
||||||
|
os.close(slave_fd)
|
||||||
|
slave_fd = None
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Set non-blocking mode for master_fd
|
# Set non-blocking mode for master_fd
|
||||||
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
||||||
@@ -573,6 +582,13 @@ def script_websocket(ws, session_id):
|
|||||||
|
|
||||||
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
|
||||||
|
# closed. The browser previously saw the worker disappear as a
|
||||||
|
# generic WebSocket failure even when the script exited with 0.
|
||||||
|
ws.send(json.dumps({
|
||||||
|
'type': 'script_complete',
|
||||||
|
'exit_code': exit_code,
|
||||||
|
}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -581,10 +597,16 @@ def script_websocket(ws, session_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = ws.receive(timeout=None)
|
data = ws.receive(timeout=0.25)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
break
|
if script_process.poll() is not None:
|
||||||
|
# The output worker owns the final PTY drain and emits
|
||||||
|
# both `[Script exited with code N]` and
|
||||||
|
# `script_complete`. Wait briefly for it before cleanup.
|
||||||
|
output_thread.join(timeout=2.0)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(data)
|
msg = json.loads(data)
|
||||||
@@ -625,6 +647,14 @@ def script_websocket(ws, session_id):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if script_process.poll() is not None:
|
if script_process.poll() is not None:
|
||||||
|
# The output worker owns the final PTY drain and emits both
|
||||||
|
# `[Script exited with code N]` and `script_complete`. A
|
||||||
|
# resize/ping arriving just after process exit used to make
|
||||||
|
# this receive loop enter cleanup immediately, closing the
|
||||||
|
# socket before those final frames were sent. Wait briefly
|
||||||
|
# for the worker so a normal script exit is delivered before
|
||||||
|
# teardown.
|
||||||
|
output_thread.join(timeout=2.0)
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -644,10 +674,11 @@ def script_websocket(ws, session_id):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
if slave_fd is not None:
|
||||||
os.close(slave_fd)
|
try:
|
||||||
except:
|
os.close(slave_fd)
|
||||||
pass
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.close(web_log_fd)
|
os.close(web_log_fd)
|
||||||
|
|||||||
Reference in New Issue
Block a user