mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 10:47:36 +00:00
New version 1.2.6
Restores AI Assistant support for OpenAI-compatible endpoints on private IPs, loopback and Docker networks (LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute, self-hosted proxies) and surfaces the server's error under the Load button (#325). Aligns the Secure Gateway wizard's Alpine template selection and pct create with the host's real architecture on x86_64 and arm64 (#324). Consolidates changes landing on develop: atomic notification delivery, custom SSH port for Borg remote targets (#236), optional GitHub API token for app version tracking (#306), and richer replication failure notifications.
This commit is contained in:
@@ -340,6 +340,12 @@ export function NotificationSettings() {
|
||||
const [testingAI, setTestingAI] = useState(false)
|
||||
const [aiTestResult, setAiTestResult] = useState<{ success: boolean; message: string; model?: string } | null>(null)
|
||||
const [providerModels, setProviderModels] = useState<string[]>([])
|
||||
// Surfaces the message the backend returns when the models fetch
|
||||
// fails (bad key, unreachable endpoint, SSRF guard blocking the URL,
|
||||
// network error). Cleared on the next successful fetch, on provider
|
||||
// change and on unmount. Without this the dropdown just goes empty
|
||||
// and the user has no clue why (issue #325).
|
||||
const [providerModelsError, setProviderModelsError] = useState<string | null>(null)
|
||||
const [loadingProviderModels, setLoadingProviderModels] = useState(false)
|
||||
const [showCustomPromptInfo, setShowCustomPromptInfo] = useState(false)
|
||||
const [editingCustomPrompt, setEditingCustomPrompt] = useState(false)
|
||||
@@ -992,6 +998,7 @@ export function NotificationSettings() {
|
||||
}
|
||||
|
||||
setLoadingProviderModels(true)
|
||||
setProviderModelsError(null)
|
||||
try {
|
||||
const data = await fetchApi<{ success: boolean; models: string[]; recommended: string; message: string }>("/api/notifications/provider-models", {
|
||||
method: "POST",
|
||||
@@ -1019,9 +1026,13 @@ export function NotificationSettings() {
|
||||
})
|
||||
} else {
|
||||
setProviderModels([])
|
||||
// Surface the backend's error message so the user can act on
|
||||
// it (bad key, SSRF-blocked URL, unreachable endpoint …).
|
||||
setProviderModelsError(data.message || t("settings.notifications.ai.loadModelsFailed"))
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
setProviderModels([])
|
||||
setProviderModelsError(err instanceof Error ? err.message : t("settings.notifications.ai.loadModelsFailed"))
|
||||
} finally {
|
||||
setLoadingProviderModels(false)
|
||||
}
|
||||
@@ -2427,6 +2438,12 @@ export function NotificationSettings() {
|
||||
{providerModels.length > 0 && (
|
||||
<p className="text-xs text-green-500">{t("settings.notifications.ai.modelsAvailable", { count: providerModels.length })}</p>
|
||||
)}
|
||||
{/* Surface the backend's error message when a Load attempt
|
||||
returns empty — silent dropdown was invisible to the user
|
||||
(issue #325). Cleared when the next successful load lands. */}
|
||||
{providerModels.length === 0 && providerModelsError && !loadingProviderModels && (
|
||||
<p className="text-xs text-red-400">{providerModelsError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Prompt Mode section */}
|
||||
|
||||
@@ -18,6 +18,23 @@ interface ReleaseNote {
|
||||
}
|
||||
|
||||
export const CHANGELOG: Record<string, ReleaseNote> = {
|
||||
"1.2.6": {
|
||||
date: "September 2, 2026",
|
||||
changes: {
|
||||
added: [
|
||||
"Borg remote target — the Add Borg destination dialog in the Monitor and the shell TUI (menu → Host Backup → New Borg target) accept a custom SSH port; the default stays at 22 and existing entries created without a port keep working. BORG_RSH, the auto key install flow and the capacity probe all honour the custom port (suggested by @songochain in discussion #236).",
|
||||
"GitHub API — Settings → GitHub API accepts an optional personal access token for release and tag checks when GitHub's anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser (suggested by @SystemIdleProcess in discussion #306).",
|
||||
],
|
||||
changed: [
|
||||
"Notification delivery is atomic — events reserve their deduplication fingerprint before AI processing and channel delivery, so concurrent collectors or parallel Monitor processes cannot send the same event twice. The reservation is shared through SQLite and released when no channel succeeds, preserving retries after temporary transport failures.",
|
||||
"Native Proxmox replication failure notifications now resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job's failures deduplicate independently (reported by Ale R.).",
|
||||
],
|
||||
fixed: [
|
||||
"AI Assistant custom OpenAI endpoint — endpoints reachable on private IPs, loopback or Docker networks (LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute, self-hosted proxies…) are accepted when loading the model catalogue and validating the AI configuration. The dropdown surfaces the reason returned by the server (or the underlying network error) directly under the Load button, in every Monitor language (#325, reported by @jorgeffonte).",
|
||||
"Secure Gateway wizard — Alpine template download and local template selection filter by the host's real architecture (via dpkg --print-architecture, falling back to uname -m); pct create is invoked with an explicit --arch so container metadata matches the host on both x86_64 and arm64 (#324, reported by @N0X4DD0).",
|
||||
],
|
||||
},
|
||||
},
|
||||
"1.2.5": {
|
||||
date: "September 1, 2026",
|
||||
changes: {
|
||||
@@ -289,28 +306,33 @@ export const CHANGELOG: Record<string, ReleaseNote> = {
|
||||
const CURRENT_VERSION_FEATURES = [
|
||||
{
|
||||
icon: <Sparkles className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.appsDashboard",
|
||||
text: "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
key: "releaseNotes.currentFeatures.aiCustomEndpoint",
|
||||
text: "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
},
|
||||
{
|
||||
icon: <Cpu className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.lxcAppsUpdates",
|
||||
text: "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
icon: <Wrench className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.secureGatewayArch",
|
||||
text: "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
},
|
||||
{
|
||||
icon: <Zap className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.appCatalog",
|
||||
text: "New application detection catalog with over 380 tracked workloads, generated live from community-scripts across seven detector methods (file, binary, dpkg, apk, Python, Docker exec, Docker label). Primary and fallback detectors cover both new and historical LXC layouts.",
|
||||
icon: <Bell className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.atomicNotifications",
|
||||
text: "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
},
|
||||
{
|
||||
icon: <Languages className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.multilingual",
|
||||
text: "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
icon: <DatabaseBackup className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.borgSshPort",
|
||||
text: "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
},
|
||||
{
|
||||
icon: <Server className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.nvidiaMultiGpu",
|
||||
text: "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.githubToken",
|
||||
text: "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
},
|
||||
{
|
||||
icon: <RefreshCw className="h-5 w-5" />,
|
||||
key: "releaseNotes.currentFeatures.replicationContext",
|
||||
text: "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.).",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1087,6 +1087,9 @@ export function VirtualMachines() {
|
||||
cache.firewall.delete(vm.vmid)
|
||||
dockerInventoryRequestedRef.current.delete(vm.vmid)
|
||||
invalidateLxcApps(vm.vmid)
|
||||
if (selectedVMRef.current?.vmid === vm.vmid) {
|
||||
setScheduleLoaded(null)
|
||||
}
|
||||
setVmConfigs((existing) => {
|
||||
if (!(vm.vmid in existing)) return existing
|
||||
const next = { ...existing }
|
||||
@@ -1830,6 +1833,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
const [scheduleLastRunAt, setScheduleLastRunAt] = useState<string | null>(null)
|
||||
const [scheduleLastRunStatus, setScheduleLastRunStatus] = useState<string | null>(null)
|
||||
const [scheduleLastRunReason, setScheduleLastRunReason] = useState<string | null>(null)
|
||||
const [scheduleLastRunLog, setScheduleLastRunLog] = useState<string | null>(null)
|
||||
const [scheduleLastRunRebootRequired, setScheduleLastRunRebootRequired] = useState(false)
|
||||
const [scheduleLastRunRebootPackages, setScheduleLastRunRebootPackages] = useState<string[]>([])
|
||||
const [scheduleLogOpen, setScheduleLogOpen] = useState(false)
|
||||
const [scheduleLogLoading, setScheduleLogLoading] = useState(false)
|
||||
const [scheduleLogContent, setScheduleLogContent] = useState("")
|
||||
const [scheduleLogError, setScheduleLogError] = useState<string | null>(null)
|
||||
const [scheduleLogTruncated, setScheduleLogTruncated] = useState(false)
|
||||
const [scheduleSaving, setScheduleSaving] = useState(false)
|
||||
const [scheduleError, setScheduleError] = useState<string | null>(null)
|
||||
const [externalCron, setExternalCron] = useState<{
|
||||
@@ -1897,6 +1908,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
setScheduleLastRunAt(s.last_run_at || null)
|
||||
setScheduleLastRunStatus(s.last_run_status || null)
|
||||
setScheduleLastRunReason(s.last_run_reason || null)
|
||||
setScheduleLastRunLog(s.last_run_log || null)
|
||||
setScheduleLastRunRebootRequired(s.last_run_reboot_required === true)
|
||||
setScheduleLastRunRebootPackages(Array.isArray(s.last_run_reboot_packages)
|
||||
? s.last_run_reboot_packages.map((value: any) => String(value))
|
||||
: [])
|
||||
setExternalCron(s.external_cron || null)
|
||||
}
|
||||
|
||||
@@ -1943,6 +1959,23 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const openScheduleLog = async (vmid: number) => {
|
||||
setScheduleLogOpen(true)
|
||||
setScheduleLogLoading(true)
|
||||
setScheduleLogContent("")
|
||||
setScheduleLogError(null)
|
||||
setScheduleLogTruncated(false)
|
||||
try {
|
||||
const payload: any = await fetchApi(`/api/vms/${vmid}/schedule/log`)
|
||||
setScheduleLogContent(String(payload?.content || ""))
|
||||
setScheduleLogTruncated(payload?.truncated === true)
|
||||
} catch (e: any) {
|
||||
setScheduleLogError(e?.message || t("vmLxc.scheduled.logFailed"))
|
||||
} finally {
|
||||
setScheduleLogLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveBulkUpdate = async (vmid: number) => {
|
||||
setBulkSaving(true)
|
||||
setBulkError(null)
|
||||
@@ -2094,6 +2127,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
setScheduleLastRunAt(null)
|
||||
setScheduleLastRunStatus(null)
|
||||
setScheduleLastRunReason(null)
|
||||
setScheduleLastRunLog(null)
|
||||
setScheduleLastRunRebootRequired(false)
|
||||
setScheduleLastRunRebootPackages([])
|
||||
setScheduleReleaseDelayDays(0)
|
||||
} catch (e: any) {
|
||||
setScheduleError(e?.message || "Delete failed")
|
||||
@@ -2127,6 +2163,59 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
return expr
|
||||
}
|
||||
|
||||
const renderScheduleRunDetails = (indented = false) => {
|
||||
if (!scheduleLastRunAt || !selectedVM) return null
|
||||
const spacing = indented ? "pl-4" : ""
|
||||
return (
|
||||
<div className={`space-y-1.5 ${spacing}`}>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })}
|
||||
{scheduleLastRunStatus && (
|
||||
<> · <span className={scheduleLastRunStatus === "success" ? "text-green-400" : scheduleLastRunStatus === "partial" || scheduleLastRunStatus === "deferred" || scheduleLastRunStatus === "skipped" ? "text-amber-400" : "text-red-400"}>
|
||||
{scheduleLastRunStatus === "success"
|
||||
? t("vmLxc.scheduled.runSuccess")
|
||||
: scheduleLastRunStatus === "partial"
|
||||
? t("vmLxc.scheduled.runPartial")
|
||||
: scheduleLastRunStatus === "deferred"
|
||||
? t("vmLxc.scheduled.runDeferred")
|
||||
: scheduleLastRunStatus === "skipped"
|
||||
? t("vmLxc.scheduled.runSkipped")
|
||||
: t("vmLxc.scheduled.runFailed")}
|
||||
</span></>
|
||||
)}
|
||||
</div>
|
||||
{scheduleLastRunReason && (
|
||||
<div className="text-xs text-muted-foreground break-words">
|
||||
{scheduleLastRunReason}
|
||||
</div>
|
||||
)}
|
||||
{scheduleLastRunRebootRequired && (
|
||||
<div className="text-xs text-amber-400 flex items-start gap-1.5">
|
||||
<AlertTriangle className="h-3.5 w-3.5 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<div>{t("vmLxc.scheduled.rebootRequired")}</div>
|
||||
{scheduleLastRunRebootPackages.length > 0 && (
|
||||
<div className="text-muted-foreground mt-0.5 break-words">
|
||||
{t("vmLxc.scheduled.rebootPackages", { packages: scheduleLastRunRebootPackages.join(", ") })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{scheduleLastRunLog && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void openScheduleLog(selectedVM.vmid)}
|
||||
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"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
{t("vmLxc.scheduled.viewLog")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Load the schedule once whenever the user opens the Updates tab
|
||||
// of a specific LXC. Keying on vmid keeps us from re-fetching on
|
||||
// every render but also refetches after switching CTs.
|
||||
@@ -2134,9 +2223,15 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
if (activeModalTab !== "updates") return
|
||||
if (!selectedVM || selectedVM.type !== "lxc") return
|
||||
if (scheduleLoaded !== selectedVM.vmid) loadSchedule(selectedVM.vmid)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeModalTab, selectedVM?.vmid, selectedVM?.modal_cache_revision, scheduleLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeModalTab !== "updates") return
|
||||
if (!selectedVM || selectedVM.type !== "lxc") return
|
||||
if (bulkLoaded !== selectedVM.vmid) loadBulkUpdate(selectedVM.vmid)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeModalTab, selectedVM?.vmid])
|
||||
}, [activeModalTab, selectedVM?.vmid, bulkLoaded])
|
||||
|
||||
// Docker drift is opt-in: read it only after Docker has been registered and
|
||||
// only when the user opens Updates. This request deliberately DOES NOT use
|
||||
@@ -6198,29 +6293,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
<> · {t("vmLxc.scheduled.releaseDelaySummary", { days: scheduleReleaseDelayDays })}</>
|
||||
)}
|
||||
</div>
|
||||
{scheduleLastRunAt && (
|
||||
<div className="text-xs text-muted-foreground pl-4">
|
||||
{t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })}
|
||||
{scheduleLastRunStatus && (
|
||||
<> · <span className={scheduleLastRunStatus === "success" ? "text-green-400" : scheduleLastRunStatus === "partial" || scheduleLastRunStatus === "deferred" || scheduleLastRunStatus === "skipped" ? "text-amber-400" : "text-red-400"}>
|
||||
{scheduleLastRunStatus === "success"
|
||||
? t("vmLxc.scheduled.runSuccess")
|
||||
: scheduleLastRunStatus === "partial"
|
||||
? t("vmLxc.scheduled.runPartial")
|
||||
: scheduleLastRunStatus === "deferred"
|
||||
? t("vmLxc.scheduled.runDeferred")
|
||||
: scheduleLastRunStatus === "skipped"
|
||||
? t("vmLxc.scheduled.runSkipped")
|
||||
: t("vmLxc.scheduled.runFailed")}
|
||||
</span></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{scheduleLastRunReason && (
|
||||
<div className="text-xs text-muted-foreground pl-4 break-words">
|
||||
{scheduleLastRunReason}
|
||||
</div>
|
||||
)}
|
||||
{renderScheduleRunDetails(true)}
|
||||
</div>
|
||||
)}
|
||||
{!optionsEditMode && !scheduleConfigured && !externalCron && (
|
||||
@@ -6351,25 +6424,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{scheduleLastRunAt && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })}
|
||||
{scheduleLastRunStatus && (
|
||||
<> · <span className={scheduleLastRunStatus === "success" ? "text-green-400" : scheduleLastRunStatus === "partial" || scheduleLastRunStatus === "deferred" || scheduleLastRunStatus === "skipped" ? "text-amber-400" : "text-red-400"}>
|
||||
{scheduleLastRunStatus === "success"
|
||||
? t("vmLxc.scheduled.runSuccess")
|
||||
: scheduleLastRunStatus === "partial"
|
||||
? t("vmLxc.scheduled.runPartial")
|
||||
: scheduleLastRunStatus === "deferred"
|
||||
? t("vmLxc.scheduled.runDeferred")
|
||||
: scheduleLastRunStatus === "skipped"
|
||||
? t("vmLxc.scheduled.runSkipped")
|
||||
: t("vmLxc.scheduled.runFailed")}
|
||||
</span></>
|
||||
)}
|
||||
{scheduleLastRunReason && <div className="mt-1 break-words">{scheduleLastRunReason}</div>}
|
||||
</div>
|
||||
)}
|
||||
{renderScheduleRunDetails()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6996,6 +7051,51 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={scheduleLogOpen} onOpenChange={setScheduleLogOpen}>
|
||||
<DialogContent className="sm:max-w-4xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
{t("vmLxc.scheduled.logTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("vmLxc.scheduled.logDescription", {
|
||||
name: selectedVM?.name || "LXC",
|
||||
vmid: selectedVM?.vmid || "—",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="min-h-0 flex-1">
|
||||
{scheduleLogLoading ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{t("vmLxc.scheduled.logLoading")}
|
||||
</div>
|
||||
) : scheduleLogError ? (
|
||||
<div className="rounded-md border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
|
||||
{scheduleLogError}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{scheduleLogTruncated && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 p-2 text-xs text-amber-400">
|
||||
{t("vmLxc.scheduled.logTruncated")}
|
||||
</div>
|
||||
)}
|
||||
<pre className="max-h-[58vh] overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-background p-4 text-xs font-mono text-foreground">
|
||||
{scheduleLogContent || t("vmLxc.scheduled.logEmpty")}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setScheduleLogOpen(false)}>
|
||||
{t("vmLxc.scheduled.closeLog")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* LXC Terminal Modal */}
|
||||
{terminalVmid !== null && (
|
||||
<LxcTerminalModal
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
// 3. beta_version.txt ← bash pipeline (build_appimage.sh)
|
||||
//
|
||||
// Keep the three in sync on every bump.
|
||||
export const APP_VERSION = "1.2.5"
|
||||
export const APP_VERSION = "1.2.6"
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Anwendung",
|
||||
"targetBoth": "Betriebssystem + Anwendung",
|
||||
"lastRun": "Letzte Ausführung: {date}",
|
||||
"rebootRequired": "Zum Abschluss der Aktualisierung ist ein Neustart erforderlich.",
|
||||
"rebootPackages": "Pakete: {packages}",
|
||||
"viewLog": "Protokoll anzeigen",
|
||||
"logTitle": "Aktualisierungsprotokoll",
|
||||
"logDescription": "Ausgabe der letzten geplanten Aktualisierung von {name} (LXC {vmid}).",
|
||||
"logLoading": "Protokoll wird geladen…",
|
||||
"logEmpty": "Dieser Lauf hat keine Ausgabe erzeugt.",
|
||||
"logFailed": "Das Aktualisierungsprotokoll konnte nicht geladen werden.",
|
||||
"logTruncated": "Es wird nur das Ende des Protokolls angezeigt, da es das Anzeigelimit überschreitet.",
|
||||
"closeLog": "Schließen",
|
||||
"runSuccess": "✓ Erfolg",
|
||||
"runPartial": "teilweise abgeschlossen",
|
||||
"runFailed": "✗ fehlgeschlagen",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "Es ist eine kostenlose Stufe mit einem guten Preis-Leistungs-Verhältnis verfügbar.",
|
||||
"ollama": "Verwendet Modelle auf Ihrem Ollama-Server. Völlig lokal, privat und kostenlos nutzbar.",
|
||||
"openrouter": "Zugriff auf mehr als 100 Modelle über einen API-Schlüssel."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Modelle konnten nicht geladen werden — prüfe den API-Schlüssel oder die Endpunkt-URL."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Anleitung zur Einrichtung des Telegram-Bots",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "Sie können auch einen Backup-Code verwenden (Format: XXXX-XXXX)",
|
||||
"backToLogin": "Zurück zum Login",
|
||||
"verifyCode": "Code überprüfen",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Angemeldet",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1163,6 +1163,16 @@
|
||||
"targetApp": "Application",
|
||||
"targetBoth": "OS + application",
|
||||
"lastRun": "Last run: {date}",
|
||||
"rebootRequired": "A restart is required to complete the update.",
|
||||
"rebootPackages": "Packages: {packages}",
|
||||
"viewLog": "View log",
|
||||
"logTitle": "Update log",
|
||||
"logDescription": "Output from the latest scheduled update of {name} (LXC {vmid}).",
|
||||
"logLoading": "Loading log…",
|
||||
"logEmpty": "This run produced no output.",
|
||||
"logFailed": "The update log could not be loaded.",
|
||||
"logTruncated": "Only the end of the log is shown because it exceeds the display limit.",
|
||||
"closeLog": "Close",
|
||||
"runSuccess": "✓ success",
|
||||
"runPartial": "completed partially",
|
||||
"runFailed": "✗ failed",
|
||||
@@ -1936,7 +1946,8 @@
|
||||
"gemini": "A free tier is available, with a good quality-to-price ratio.",
|
||||
"ollama": "Uses models on your Ollama server. Fully local, private and free to run.",
|
||||
"openrouter": "Access to more than 100 models through one API key."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Failed to load models — check the API key or endpoint URL."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Telegram bot setup guide",
|
||||
@@ -2101,7 +2112,7 @@
|
||||
"backupCodeHint": "You can also use a backup code (format: XXXX-XXXX)",
|
||||
"backToLogin": "Back to login",
|
||||
"verifyCode": "Verify Code",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Signed in",
|
||||
@@ -3157,7 +3168,13 @@
|
||||
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Solicitud",
|
||||
"targetBoth": "SO + aplicación",
|
||||
"lastRun": "Última ejecución: {date}",
|
||||
"rebootRequired": "Es necesario reiniciar para completar la actualización.",
|
||||
"rebootPackages": "Paquetes: {packages}",
|
||||
"viewLog": "Ver log",
|
||||
"logTitle": "Log de actualización",
|
||||
"logDescription": "Salida de la última actualización programada de {name} (LXC {vmid}).",
|
||||
"logLoading": "Cargando log…",
|
||||
"logEmpty": "Esta ejecución no produjo ninguna salida.",
|
||||
"logFailed": "No se pudo cargar el log de actualización.",
|
||||
"logTruncated": "Solo se muestra el final del log porque supera el límite de visualización.",
|
||||
"closeLog": "Cerrar",
|
||||
"runSuccess": "✓ éxito",
|
||||
"runPartial": "completada parcialmente",
|
||||
"runFailed": "✗ falló",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "Hay disponible un nivel gratuito, con una buena relación calidad-precio.",
|
||||
"ollama": "Utiliza modelos en su servidor Ollama. Totalmente local, privado y gratuito.",
|
||||
"openrouter": "Acceso a más de 100 modelos a través de una clave API."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "No se pudieron cargar los modelos — comprueba la API key o la URL del endpoint."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Guía de configuración del bot de Telegram",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "También puedes utilizar un código de respaldo (formato: XXXX-XXXX)",
|
||||
"backToLogin": "Volver a iniciar sesión",
|
||||
"verifyCode": "Verificar código",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Iniciado sesión",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "Nueva pestaña Apps de nivel superior — un lanzador único para cada enlace web del nodo. Las aplicaciones registradas en LXC y los enlaces web personalizados comparten la misma cuadrícula con etiquetas de categoría, búsqueda y acceso directo al modal del invitado.",
|
||||
"lxcAppsUpdates": "La pestaña App dentro del modal de cada LXC registra las aplicaciones instaladas, captura sus enlaces web y realiza seguimiento de versiones. La pestaña Updates rediseñada aplica actualizaciones de paquetes del sistema y de aplicaciones desde un solo botón; Docker Engine y cada imagen siguen el mismo ciclo de 24 horas, con acción 'Comprobar ahora' bajo demanda.",
|
||||
"multilingual": "El Monitor ahora habla 8 idiomas: inglés, español, alemán, francés, italiano, portugués, sueco y eslovaco. Un enorme agradecimiento a @vaso73 por construir la base de i18n que lo hizo posible.",
|
||||
"nvidiaMultiGpu": "El ciclo de vida del driver NVIDIA pasa a propiedad por BDF exacto, de modo que un host multi-GPU puede pasar una tarjeta a una VM y mantener la otra operativa en el host o en LXCs, junto con un selector de versión sensible al kernel, la rama y la GPU (#298)."
|
||||
"nvidiaMultiGpu": "El ciclo de vida del driver NVIDIA pasa a propiedad por BDF exacto, de modo que un host multi-GPU puede pasar una tarjeta a una VM y mantener la otra operativa en el host o en LXCs, junto con un selector de versión sensible al kernel, la rama y la GPU (#298).",
|
||||
"aiCustomEndpoint": "Endpoint OpenAI personalizado del Asistente IA — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute y cualquier proxy autoalojado en IPs privadas, loopback o redes Docker se reconocen al cargar el catálogo de modelos. El desplegable muestra el error devuelto por el servidor (o el motivo de red subyacente) justo debajo del botón Cargar (#325, reportado por @jorgeffonte).",
|
||||
"secureGatewayArch": "Asistente Secure Gateway — la descarga de plantilla Alpine, la selección de plantilla local y pct create coinciden con la arquitectura real del host, de modo que los hosts x86_64 reciben contenedores amd64 y los hosts arm64 reciben contenedores arm64 (#324, reportado por @N0X4DD0).",
|
||||
"atomicNotifications": "Los eventos de notificación reservan su huella de deduplicación de forma atómica antes del procesado por IA y del envío por canal, así que colectores concurrentes, callbacks de finalización o procesos Monitor paralelos no pueden enviar el mismo evento dos veces. La reserva se libera cuando ningún canal tiene éxito, preservando los reintentos.",
|
||||
"borgSshPort": "Destino remoto Borg — el diálogo Añadir destino Borg y el TUI del shell aceptan un puerto SSH personalizado. BORG_RSH, el flujo de instalación automática de clave y la sonda de capacidad lo respetan. Totalmente retrocompatible con las entradas existentes creadas sin un puerto explícito (sugerido por @songochain en la discusión #236).",
|
||||
"githubToken": "Settings → GitHub API acepta un token de acceso personal opcional para las comprobaciones de releases y tags cuando se agota la cuota anónima. El token se guarda cifrado y nunca se devuelve al navegador; el error de rate limit está traducido en todos los idiomas del Monitor (sugerido por @SystemIdleProcess en la discusión #306).",
|
||||
"replicationContext": "Las notificaciones nativas de fallo de replicación de Proxmox ahora resuelven el ID del trabajo de replicación, el ID de la VM/LXC afectada y el nombre del guest; el bloque de error exacto de Proxmox se conserva como motivo, y cada trabajo de replicación deduplica de forma independiente (reportado por Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Application",
|
||||
"targetBoth": "Système d'exploitation + application",
|
||||
"lastRun": "Dernière exécution : {date}",
|
||||
"rebootRequired": "Un redémarrage est nécessaire pour terminer la mise à jour.",
|
||||
"rebootPackages": "Paquets : {packages}",
|
||||
"viewLog": "Voir le journal",
|
||||
"logTitle": "Journal de mise à jour",
|
||||
"logDescription": "Sortie de la dernière mise à jour planifiée de {name} (LXC {vmid}).",
|
||||
"logLoading": "Chargement du journal…",
|
||||
"logEmpty": "Cette exécution n’a produit aucune sortie.",
|
||||
"logFailed": "Impossible de charger le journal de mise à jour.",
|
||||
"logTruncated": "Seule la fin du journal est affichée, car il dépasse la limite d’affichage.",
|
||||
"closeLog": "Fermer",
|
||||
"runSuccess": "✓ succès",
|
||||
"runPartial": "partiellement terminée",
|
||||
"runFailed": "✗ échoué",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "Un niveau gratuit est disponible, avec un bon rapport qualité-prix.",
|
||||
"ollama": "Utilise des modèles sur votre serveur Ollama. Entièrement local, privé et gratuit.",
|
||||
"openrouter": "Accès à plus de 100 modèles via une seule clé API."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Impossible de charger les modèles — vérifie la clé API ou l'URL du endpoint."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Guide de configuration du robot Telegram",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "Vous pouvez également utiliser un code de secours (format : XXXX-XXXX)",
|
||||
"backToLogin": "Retour à la connexion",
|
||||
"verifyCode": "Vérifier le code",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Connecté",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Applicazione",
|
||||
"targetBoth": "Sistema operativo + applicazione",
|
||||
"lastRun": "Ultima esecuzione: {date}",
|
||||
"rebootRequired": "È necessario un riavvio per completare l’aggiornamento.",
|
||||
"rebootPackages": "Pacchetti: {packages}",
|
||||
"viewLog": "Visualizza log",
|
||||
"logTitle": "Log di aggiornamento",
|
||||
"logDescription": "Output dell’ultimo aggiornamento pianificato di {name} (LXC {vmid}).",
|
||||
"logLoading": "Caricamento del log…",
|
||||
"logEmpty": "Questa esecuzione non ha prodotto alcun output.",
|
||||
"logFailed": "Impossibile caricare il log di aggiornamento.",
|
||||
"logTruncated": "Viene mostrata solo la parte finale del log perché supera il limite di visualizzazione.",
|
||||
"closeLog": "Chiudi",
|
||||
"runSuccess": "✓ successo",
|
||||
"runPartial": "completato parzialmente",
|
||||
"runFailed": "✗ fallito",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "È disponibile un livello gratuito, con un buon rapporto qualità-prezzo.",
|
||||
"ollama": "Utilizza i modelli sul tuo server Ollama. Completamente locale, privato e gratuito.",
|
||||
"openrouter": "Accesso a più di 100 modelli tramite una chiave API."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Impossibile caricare i modelli — controlla la API key o l'URL dell'endpoint."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Guida alla configurazione del bot di Telegram",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "Puoi anche utilizzare un codice di backup (formato: XXXX-XXXX)",
|
||||
"backToLogin": "Torna al login",
|
||||
"verifyCode": "Verifica codice",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Effettuato l'accesso",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Aplicativo",
|
||||
"targetBoth": "SO + aplicativo",
|
||||
"lastRun": "Última execução: {date}",
|
||||
"rebootRequired": "É necessário reiniciar para concluir a atualização.",
|
||||
"rebootPackages": "Pacotes: {packages}",
|
||||
"viewLog": "Ver registo",
|
||||
"logTitle": "Registo da atualização",
|
||||
"logDescription": "Saída da última atualização agendada de {name} (LXC {vmid}).",
|
||||
"logLoading": "A carregar o registo…",
|
||||
"logEmpty": "Esta execução não produziu qualquer saída.",
|
||||
"logFailed": "Não foi possível carregar o registo da atualização.",
|
||||
"logTruncated": "É apresentado apenas o fim do registo porque excede o limite de visualização.",
|
||||
"closeLog": "Fechar",
|
||||
"runSuccess": "✓ sucesso",
|
||||
"runPartial": "concluída parcialmente",
|
||||
"runFailed": "✗ falhou",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "Um nível gratuito está disponível, com uma boa relação qualidade/preço.",
|
||||
"ollama": "Usa modelos em seu servidor Ollama. Totalmente local, privado e de operação gratuita.",
|
||||
"openrouter": "Acesso a mais de 100 modelos através de uma chave API."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Não foi possível carregar os modelos — verifica a API key ou o URL do endpoint."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Guia de configuração do bot do Telegram",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "Você também pode usar um código de backup (formato: XXXX-XXXX)",
|
||||
"backToLogin": "Voltar ao login",
|
||||
"verifyCode": "Verifique o código",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Conectado",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "New top-level Apps dashboard — a single launcher for every Web Link across the node. LXC-registered apps and user-defined Custom Web Links share the same grid with category badges, search, and one-click deep-links back to the guest modal.",
|
||||
"lxcAppsUpdates": "App tab inside every LXC modal registers installed apps, captures weblinks and tracks upstream versions. Reworked Updates tab applies OS packages and app updates from a single button; Docker Engine and per-image tracking follow the same 24-hour cycle, with a 'Check now' action on demand.",
|
||||
"multilingual": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA driver lifecycle moves to per-BDF ownership so a multi-GPU host can pass one card to a VM and keep the other operational on the host or in LXCs, plus a kernel + branch + GPU-aware version picker (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1163,6 +1163,16 @@
|
||||
"targetApp": "Aplikácia",
|
||||
"targetBoth": "Systém + aplikácia",
|
||||
"lastRun": "Posledné spustenie: {date}",
|
||||
"rebootRequired": "Na dokončenie aktualizácie je potrebný reštart.",
|
||||
"rebootPackages": "Balíky: {packages}",
|
||||
"viewLog": "Zobraziť záznam",
|
||||
"logTitle": "Záznam aktualizácie",
|
||||
"logDescription": "Výstup poslednej naplánovanej aktualizácie {name} (LXC {vmid}).",
|
||||
"logLoading": "Načítava sa záznam…",
|
||||
"logEmpty": "Toto spustenie nevytvorilo žiadny výstup.",
|
||||
"logFailed": "Záznam aktualizácie sa nepodarilo načítať.",
|
||||
"logTruncated": "Zobrazuje sa iba koniec záznamu, pretože prekračuje limit zobrazenia.",
|
||||
"closeLog": "Zavrieť",
|
||||
"runSuccess": "✓ úspešné",
|
||||
"runPartial": "čiastočne dokončené",
|
||||
"runFailed": "✗ zlyhalo",
|
||||
@@ -1936,7 +1946,8 @@
|
||||
"gemini": "Ponúka bezplatnú úroveň a dobrý pomer kvality a ceny.",
|
||||
"ollama": "Používa modely na vašom Ollama serveri. Beží lokálne, súkromne a bez poplatkov.",
|
||||
"openrouter": "Prístup k viac než 100 modelom cez jeden API kľúč."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Modely sa nepodarilo načítať — skontroluj API kľúč alebo URL endpointu."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Nastavenie Telegram bota",
|
||||
@@ -2101,7 +2112,7 @@
|
||||
"backupCodeHint": "Môžete použiť aj záložný kód vo formáte XXXX-XXXX",
|
||||
"backToLogin": "Späť na prihlásenie",
|
||||
"verifyCode": "Overiť kód",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Prihlásený",
|
||||
@@ -3157,7 +3168,13 @@
|
||||
"appsDashboard": "Nová hlavná karta Apps — jednotný spúšťač pre každý webový odkaz v uzle. Aplikácie zaregistrované v LXC a používateľské vlastné webové odkazy zdieľajú rovnakú mriežku s kategóriami, vyhľadávaním a priamym prístupom do modálu hostiteľa.",
|
||||
"lxcAppsUpdates": "Karta App v modáli každého LXC registruje nainštalované aplikácie, zachytáva webové odkazy a sleduje verzie. Prepracovaná karta Updates aplikuje aktualizácie OS a aplikácií jediným tlačidlom; Docker Engine a jednotlivé image sledujú rovnaký 24-hodinový cyklus s akciou 'Skontrolovať teraz' na požiadanie.",
|
||||
"multilingual": "Monitor teraz hovorí 8 jazykmi: angličtina, španielčina, nemčina, francúzština, taliančina, portugalčina, švédčina a slovenčina. Veľká vďaka patrí @vaso73 za vybudovanie i18n základov.",
|
||||
"nvidiaMultiGpu": "Životný cyklus NVIDIA driverov prechádza na vlastníctvo podľa presného BDF, takže multi-GPU hostiteľ môže odovzdať jednu kartu do VM a druhú nechať funkčnú v hostiteľovi alebo v LXC, plus výber verzie citlivý na kernel, vetvu a GPU (#298)."
|
||||
"nvidiaMultiGpu": "Životný cyklus NVIDIA driverov prechádza na vlastníctvo podľa presného BDF, takže multi-GPU hostiteľ môže odovzdať jednu kartu do VM a druhú nechať funkčnú v hostiteľovi alebo v LXC, plus výber verzie citlivý na kernel, vetvu a GPU (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
@@ -1164,6 +1164,16 @@
|
||||
"targetApp": "Tillämpningen",
|
||||
"targetBoth": "OS + applikation",
|
||||
"lastRun": "Senaste körningen: {date}",
|
||||
"rebootRequired": "En omstart krävs för att slutföra uppdateringen.",
|
||||
"rebootPackages": "Paket: {packages}",
|
||||
"viewLog": "Visa logg",
|
||||
"logTitle": "Uppdateringslogg",
|
||||
"logDescription": "Utdata från den senaste schemalagda uppdateringen av {name} (LXC {vmid}).",
|
||||
"logLoading": "Läser in loggen…",
|
||||
"logEmpty": "Den här körningen gav ingen utdata.",
|
||||
"logFailed": "Det gick inte att läsa in uppdateringsloggen.",
|
||||
"logTruncated": "Endast slutet av loggen visas eftersom den överskrider visningsgränsen.",
|
||||
"closeLog": "Stäng",
|
||||
"runSuccess": "✓ framgång",
|
||||
"runPartial": "delvis slutförd",
|
||||
"runFailed": "✗ misslyckades",
|
||||
@@ -1937,7 +1947,8 @@
|
||||
"gemini": "En gratis nivå är tillgänglig, med ett bra förhållande mellan kvalitet och pris.",
|
||||
"ollama": "Använder modeller på din Ollama-server. Helt lokalt, privat och gratis att köra.",
|
||||
"openrouter": "Tillgång till mer än 100 modeller via en API-nyckel."
|
||||
}
|
||||
},
|
||||
"loadModelsFailed": "Kunde inte ladda modeller — kontrollera API-nyckeln eller endpoint-URL:en."
|
||||
},
|
||||
"telegramGuide": {
|
||||
"title": "Installationsguide för Telegram bot",
|
||||
@@ -2102,7 +2113,7 @@
|
||||
"backupCodeHint": "Du kan också använda en reservkod (format: XXXX-XXXX)",
|
||||
"backToLogin": "Tillbaka till inloggning",
|
||||
"verifyCode": "Verifiera koden",
|
||||
"version": "ProxMenux Monitor v1.2.5"
|
||||
"version": "ProxMenux Monitor v1.2.6"
|
||||
},
|
||||
"account": {
|
||||
"signedIn": "Inloggad",
|
||||
@@ -3158,7 +3169,13 @@
|
||||
"appsDashboard": "Ny huvudflik Apps — en enda startpunkt för varje webblänk på noden. LXC-registrerade appar och användardefinierade Custom Web Links delar samma rutnät med kategori-taggar, sökning och direktlänk till gästens modal.",
|
||||
"lxcAppsUpdates": "App-fliken inuti varje LXC-modal registrerar installerade appar, fångar webblänkar och spårar uppströmsversioner. Omarbetad Updates-flik applicerar OS-paket och appuppdateringar med en enda knapp; Docker Engine och per-image följer samma 24-timmarscykel med en 'Kontrollera nu'-åtgärd på begäran.",
|
||||
"multilingual": "Monitorn talar nu 8 språk: engelska, spanska, tyska, franska, italienska, portugisiska, svenska och slovakiska. Ett stort tack till @vaso73 för att ha byggt i18n-grunden.",
|
||||
"nvidiaMultiGpu": "NVIDIA-driverns livscykel går över till ägarskap per exakt BDF, så att en multi-GPU-värd kan skicka ett kort till en VM och behålla det andra operativt på värden eller i LXC, plus en versionsväljare som är medveten om kärna, gren och GPU (#298)."
|
||||
"nvidiaMultiGpu": "NVIDIA-driverns livscykel går över till ägarskap per exakt BDF, så att en multi-GPU-värd kan skicka ett kort till en VM och behålla det andra operativt på värden eller i LXC, plus en versionsväljare som är medveten om kärna, gren och GPU (#298).",
|
||||
"aiCustomEndpoint": "AI Assistant custom OpenAI endpoint — LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute and any self-hosted proxy on private IPs, loopback or Docker networks are recognised when loading the model catalogue. The dropdown surfaces the server's error (or the underlying network reason) directly under the Load button (#325, reported by @jorgeffonte).",
|
||||
"secureGatewayArch": "Secure Gateway wizard — the Alpine template download, local template selection and pct create all match the host's real architecture, so x86_64 hosts receive amd64 containers and arm64 hosts receive arm64 containers (#324, reported by @N0X4DD0).",
|
||||
"atomicNotifications": "Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or parallel Monitor processes cannot send the same event twice. The reservation is released when no channel succeeds, preserving retries.",
|
||||
"borgSshPort": "Borg remote target — the Add Borg destination dialog and the shell TUI accept a custom SSH port. BORG_RSH, the auto key install flow and the capacity probe all honour it. Fully backwards compatible with existing entries created without an explicit port (suggested by @songochain in discussion #236).",
|
||||
"githubToken": "Settings → GitHub API accepts an optional personal access token for release and tag checks when the anonymous quota is exhausted. The token is encrypted at rest and never returned to the browser; the rate-limit error is translated in every Monitor language (suggested by @SystemIdleProcess in discussion #306).",
|
||||
"replicationContext": "Native Proxmox replication failure notifications resolve the replication job ID, affected VM/LXC ID and guest name; the exact error block from Proxmox is preserved as the reason, and each replication job deduplicates independently (reported by Ale R.)."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ProxMenux-Monitor",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ProxMenux-Monitor",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ProxMenux-Monitor",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -471,7 +471,14 @@ def get_provider_models():
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'models': [], 'message': f'Invalid ollama_url: {err}'}), 400
|
||||
if provider == 'openai' and openai_base_url:
|
||||
ok, err = validate_external_url(openai_base_url, allow_loopback=False)
|
||||
# OpenAI-compatible endpoints (LiteLLM, LM Studio, Ollama-proxy,
|
||||
# LocalAI, vLLM, OmniRoute, opencode.ai, …) are LOCAL by design:
|
||||
# documented deployments run on localhost, on the same LAN or
|
||||
# inside a Docker network — all of which use loopback or
|
||||
# RFC1918 addresses. Blocking them made the "Custom Base URL"
|
||||
# feature unusable in practice (issue #325). The AWS metadata
|
||||
# host stays blocked via _SSRF_BLOCKED_HOSTS regardless.
|
||||
ok, err = validate_external_url(openai_base_url, allow_loopback=True)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'models': [], 'message': f'Invalid openai_base_url: {err}'}), 400
|
||||
|
||||
@@ -665,7 +672,14 @@ def test_ai_connection():
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'message': f'Invalid ollama_url: {err}', 'model': ''}), 400
|
||||
if provider == 'openai' and openai_base_url:
|
||||
ok, err = validate_external_url(openai_base_url, allow_loopback=False)
|
||||
# OpenAI-compatible endpoints (LiteLLM, LM Studio, Ollama-proxy,
|
||||
# LocalAI, vLLM, OmniRoute, opencode.ai, …) are LOCAL by design:
|
||||
# documented deployments run on localhost, on the same LAN or
|
||||
# inside a Docker network — all of which use loopback or
|
||||
# RFC1918 addresses. Blocking them made the "Custom Base URL"
|
||||
# feature unusable in practice (issue #325). The AWS metadata
|
||||
# host stays blocked via _SSRF_BLOCKED_HOSTS regardless.
|
||||
ok, err = validate_external_url(openai_base_url, allow_loopback=True)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'message': f'Invalid openai_base_url: {err}', 'model': ''}), 400
|
||||
|
||||
|
||||
@@ -2020,6 +2020,20 @@ def _handle_guest_lifecycle(vmid: str, vm_type: str, action: str) -> None:
|
||||
if guest_type == 'lxc':
|
||||
_invalidate_lxc_ip(guest_id)
|
||||
if action in ('start', 'reboot'):
|
||||
if guest_type == 'lxc':
|
||||
# TaskWatcher already owns the authoritative lifecycle event.
|
||||
# Reuse it to retire a reboot-required warning left by the last
|
||||
# scheduled update instead of adding another polling loop.
|
||||
try:
|
||||
import lxc_apps
|
||||
lxc_apps.clear_schedule_reboot_required(guest_id)
|
||||
_vm_cache_invalidate(guest_id, _vm_schedule_cache)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f'[ProxMenux] could not clear scheduled-update reboot state '
|
||||
f'for CT {guest_id}: {exc}',
|
||||
flush=True,
|
||||
)
|
||||
_schedule_started_guest_refresh(guest_id, guest_type)
|
||||
return
|
||||
if action == 'stop':
|
||||
@@ -13626,6 +13640,42 @@ def api_vm_apps_schedule(vmid):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/vms/<int:vmid>/schedule/log', methods=['GET'])
|
||||
@require_auth
|
||||
def api_vm_apps_schedule_log(vmid):
|
||||
"""Return the bounded tail of the latest scheduled-update log."""
|
||||
try:
|
||||
import lxc_apps
|
||||
schedule = lxc_apps.get_schedule(vmid) or {}
|
||||
except Exception as exc:
|
||||
return jsonify({'error': f'lxc_apps unavailable: {exc}'}), 500
|
||||
name = os.path.basename(str(schedule.get('last_run_log') or ''))
|
||||
if not _LXC_UPDATE_LOG_RE.fullmatch(name) or not name.startswith(f'{vmid}-'):
|
||||
return jsonify({'error': 'no scheduled update log is available'}), 404
|
||||
path = os.path.join(_LXC_UPDATE_LOG_DIR, name)
|
||||
if not os.path.isfile(path):
|
||||
return jsonify({'error': 'scheduled update log was not found'}), 404
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
offset = max(0, size - _LXC_UPDATE_LOG_READ_LIMIT)
|
||||
with open(path, 'rb') as stream:
|
||||
stream.seek(offset)
|
||||
content = stream.read(_LXC_UPDATE_LOG_READ_LIMIT).decode('utf-8', errors='replace')
|
||||
if offset:
|
||||
newline = content.find('\n')
|
||||
if newline >= 0:
|
||||
content = content[newline + 1:]
|
||||
return jsonify({
|
||||
'content': content,
|
||||
'size': size,
|
||||
'truncated': bool(offset),
|
||||
'run_at': schedule.get('last_run_at'),
|
||||
'status': schedule.get('last_run_status'),
|
||||
})
|
||||
except OSError as exc:
|
||||
return jsonify({'error': str(exc)}), 500
|
||||
|
||||
|
||||
@app.route('/api/vms/<int:vmid>/bulk-update', methods=['GET', 'PUT', 'DELETE'])
|
||||
@require_auth
|
||||
def api_vm_bulk_update(vmid):
|
||||
@@ -14098,6 +14148,8 @@ def _lxc_update_details(
|
||||
after: dict,
|
||||
verification_pending: bool,
|
||||
verification_errors: list[str],
|
||||
reboot_required: bool | None,
|
||||
reboot_packages: list[str],
|
||||
) -> str:
|
||||
lines = [
|
||||
f"Source: {'Scheduled' if source == 'scheduled' else 'Manual'}",
|
||||
@@ -14170,6 +14222,12 @@ def _lxc_update_details(
|
||||
lines.append('Deferred targets: ' + ', '.join(deferred_targets))
|
||||
if reason:
|
||||
lines.append(f'Reason: {reason}')
|
||||
if reboot_required is True:
|
||||
lines.append('Restart required: yes')
|
||||
if reboot_packages:
|
||||
lines.append('Restart-triggering packages: ' + ', '.join(reboot_packages[:12]))
|
||||
elif reboot_required is False:
|
||||
lines.append('Restart required: no')
|
||||
if verification_pending:
|
||||
lines.append('Verification pending until the container is running')
|
||||
for error in verification_errors[:4]:
|
||||
@@ -14194,6 +14252,8 @@ def _finalize_lxc_update(
|
||||
reason: str | None = None,
|
||||
refresh_docker_inventory: bool = False,
|
||||
before_snapshot: dict | None = None,
|
||||
reboot_required: bool | None = None,
|
||||
reboot_packages=None,
|
||||
) -> dict:
|
||||
safe_run_id = _normalise_lxc_update_run_id(run_id)
|
||||
key = (int(vmid), safe_run_id)
|
||||
@@ -14273,6 +14333,8 @@ def _finalize_lxc_update(
|
||||
after=after,
|
||||
verification_pending=verification_pending,
|
||||
verification_errors=verification_errors,
|
||||
reboot_required=reboot_required,
|
||||
reboot_packages=list(reboot_packages or []),
|
||||
)
|
||||
try:
|
||||
notification_manager.emit_event(
|
||||
@@ -14303,6 +14365,8 @@ def _finalize_lxc_update(
|
||||
'verification_pending': verification_pending,
|
||||
'verification_errors': verification_errors,
|
||||
'docker_inventory': docker_inventory,
|
||||
'reboot_required': reboot_required,
|
||||
'reboot_packages': list(reboot_packages or []),
|
||||
}
|
||||
with _lxc_update_finalization_lock:
|
||||
_lxc_update_finalizations[key] = {
|
||||
@@ -21578,6 +21642,97 @@ _DOCKER_ENGINE_INTEGRATED_COMMAND = (
|
||||
'update_docker_engine.py --vmid "$VMID"'
|
||||
)
|
||||
_scheduled_fired_this_minute: set = set()
|
||||
_LXC_UPDATE_LOG_DIR = "/usr/local/share/proxmenux/logs/lxc-updates"
|
||||
_LXC_UPDATE_LOG_RE = re.compile(r'^[1-9][0-9]*-scheduled-[a-f0-9]{32}\.log$')
|
||||
_LXC_UPDATE_LOG_KEEP_PER_CT = 10
|
||||
_LXC_UPDATE_LOG_READ_LIMIT = 2 * 1024 * 1024
|
||||
|
||||
|
||||
def _create_lxc_update_log(vmid: int, run_id: str) -> tuple[str | None, str | None]:
|
||||
"""Create a private, persistent log for one scheduled LXC run."""
|
||||
name = f'{int(vmid)}-{run_id}.log'
|
||||
if not _LXC_UPDATE_LOG_RE.fullmatch(name):
|
||||
return None, None
|
||||
try:
|
||||
os.makedirs(_LXC_UPDATE_LOG_DIR, mode=0o700, exist_ok=True)
|
||||
path = os.path.join(_LXC_UPDATE_LOG_DIR, name)
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8', errors='replace') as stream:
|
||||
stream.write(f'=== ProxMenux scheduled LXC update — CT {vmid} ===\n')
|
||||
stream.write(f'Run ID: {run_id}\n')
|
||||
stream.write(f'Started: {datetime.now().astimezone().isoformat()}\n\n')
|
||||
return name, path
|
||||
except OSError as exc:
|
||||
print(f'[ProxMenux] scheduler: could not create update log for CT {vmid}: {exc}',
|
||||
flush=True)
|
||||
return None, None
|
||||
|
||||
|
||||
def _append_lxc_update_log(path: str | None, text: str) -> None:
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
with open(path, 'a', encoding='utf-8', errors='replace') as stream:
|
||||
stream.write(text)
|
||||
except OSError as exc:
|
||||
print(f'[ProxMenux] scheduler: could not append update log: {exc}', flush=True)
|
||||
|
||||
|
||||
def _prune_lxc_update_logs(vmid: int) -> None:
|
||||
try:
|
||||
candidates = sorted(
|
||||
glob.glob(os.path.join(_LXC_UPDATE_LOG_DIR, f'{int(vmid)}-scheduled-*.log')),
|
||||
key=os.path.getmtime,
|
||||
reverse=True,
|
||||
)
|
||||
for path in candidates[_LXC_UPDATE_LOG_KEEP_PER_CT:]:
|
||||
if _LXC_UPDATE_LOG_RE.fullmatch(os.path.basename(path)):
|
||||
os.unlink(path)
|
||||
except OSError as exc:
|
||||
print(f'[ProxMenux] scheduler: update-log retention failed for CT {vmid}: {exc}',
|
||||
flush=True)
|
||||
|
||||
|
||||
def _inspect_lxc_reboot_requirement(
|
||||
vmid: int,
|
||||
*,
|
||||
update_succeeded: bool,
|
||||
restart_requested: bool,
|
||||
originally_running: bool,
|
||||
) -> tuple[bool | None, list[str], str | None]:
|
||||
"""Read Debian's reboot marker without installing extra guest tools."""
|
||||
if update_succeeded and (restart_requested or not originally_running):
|
||||
return False, [], None
|
||||
if _fast_guest_status(vmid, 'lxc') != 'running':
|
||||
return None, [], 'container is not running; reboot marker could not be checked'
|
||||
try:
|
||||
marker = subprocess.run(
|
||||
['/usr/sbin/pct', 'exec', str(vmid), '--',
|
||||
'test', '-f', '/var/run/reboot-required'],
|
||||
capture_output=True, text=True, timeout=8,
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as exc:
|
||||
return None, [], str(exc)
|
||||
if marker.returncode == 1:
|
||||
return False, [], None
|
||||
if marker.returncode != 0:
|
||||
return None, [], (marker.stderr or marker.stdout or 'reboot marker check failed').strip()[:300]
|
||||
packages: list[str] = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['/usr/sbin/pct', 'exec', str(vmid), '--',
|
||||
'cat', '/var/run/reboot-required.pkgs'],
|
||||
capture_output=True, text=True, timeout=8,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
packages = list(dict.fromkeys(
|
||||
line.strip()[:160]
|
||||
for line in result.stdout.splitlines()
|
||||
if line.strip()
|
||||
))[:32]
|
||||
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return True, packages, None
|
||||
|
||||
|
||||
def _normalise_schedule_targets(sched: dict) -> list[str]:
|
||||
@@ -21908,16 +22063,40 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
"""Run one scheduled update and finalize it through the shared path."""
|
||||
started_at = time.monotonic()
|
||||
run_id = f'scheduled-{uuid.uuid4().hex}'
|
||||
log_name, log_path = _create_lxc_update_log(vmid, run_id)
|
||||
originally_running = _fast_guest_status(vmid, 'lxc') == 'running'
|
||||
requested_targets = _normalise_schedule_targets(sched)
|
||||
targets = list(requested_targets)
|
||||
before = _lxc_update_snapshot(vmid)
|
||||
deferred_targets: list[str] = []
|
||||
reasons: list[str] = []
|
||||
reboot_required: bool | None = None
|
||||
reboot_packages: list[str] = []
|
||||
reboot_check_error: str | None = None
|
||||
|
||||
def finish(status: str, actual_target: str, executed: list[str]) -> dict:
|
||||
reason = '; '.join(dict.fromkeys(value for value in reasons if value)) or None
|
||||
duration_seconds = max(0, int(time.monotonic() - started_at))
|
||||
labels = _lxc_update_target_labels(requested_targets, [], before)
|
||||
footer = [
|
||||
'',
|
||||
'=== ProxMenux result ===',
|
||||
f'Finished: {datetime.now().astimezone().isoformat()}',
|
||||
f'Status: {status}',
|
||||
f'Duration: {duration_seconds}s',
|
||||
]
|
||||
if reason:
|
||||
footer.append(f'Reason: {reason}')
|
||||
if reboot_required is True:
|
||||
footer.append('Restart required: yes')
|
||||
if reboot_packages:
|
||||
footer.append('Restart-triggering packages: ' + ', '.join(reboot_packages))
|
||||
elif reboot_required is False:
|
||||
footer.append('Restart required: no')
|
||||
elif reboot_check_error:
|
||||
footer.append(f'Restart check unavailable: {reboot_check_error}')
|
||||
_append_lxc_update_log(log_path, '\n'.join(footer) + '\n')
|
||||
_prune_lxc_update_logs(vmid)
|
||||
finalization = _finalize_lxc_update(
|
||||
vmid,
|
||||
run_id=run_id,
|
||||
@@ -21935,6 +22114,8 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
value.startswith('docker-') for value in requested_targets
|
||||
),
|
||||
before_snapshot=before,
|
||||
reboot_required=reboot_required,
|
||||
reboot_packages=reboot_packages,
|
||||
)
|
||||
return {
|
||||
'status': status,
|
||||
@@ -21945,6 +22126,9 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
'executed_targets': list(executed),
|
||||
'deferred_targets': list(deferred_targets),
|
||||
'duration_seconds': duration_seconds,
|
||||
'log_name': log_name,
|
||||
'reboot_required': reboot_required,
|
||||
'reboot_packages': list(reboot_packages),
|
||||
'finalization': finalization,
|
||||
}
|
||||
|
||||
@@ -22063,13 +22247,32 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
"1" if any(value.startswith('docker-') for value in requested_targets) else "0"
|
||||
)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["bash", _APPLY_UPDATES_SCRIPT],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60 * 60, # 1h hard cap so a stuck run doesn't
|
||||
# block the queue forever
|
||||
if log_path:
|
||||
with open(log_path, 'a', encoding='utf-8', errors='replace') as log_stream:
|
||||
r = subprocess.run(
|
||||
["bash", _APPLY_UPDATES_SCRIPT],
|
||||
env=env,
|
||||
stdout=log_stream,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=60 * 60, # 1h hard cap so a stuck run doesn't
|
||||
# block the queue forever
|
||||
)
|
||||
else:
|
||||
r = subprocess.run(
|
||||
["bash", _APPLY_UPDATES_SCRIPT],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60 * 60,
|
||||
)
|
||||
reboot_required, reboot_packages, reboot_check_error = (
|
||||
_inspect_lxc_reboot_requirement(
|
||||
vmid,
|
||||
update_succeeded=r.returncode == 0,
|
||||
restart_requested=bool(sched.get("restart")),
|
||||
originally_running=originally_running,
|
||||
)
|
||||
)
|
||||
if r.returncode != 0:
|
||||
reasons.append(f'update runner exited with code {r.returncode}')
|
||||
@@ -22078,6 +22281,14 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
return finish('partial', target, targets)
|
||||
return finish('success', target, targets)
|
||||
except subprocess.TimeoutExpired:
|
||||
reboot_required, reboot_packages, reboot_check_error = (
|
||||
_inspect_lxc_reboot_requirement(
|
||||
vmid,
|
||||
update_succeeded=False,
|
||||
restart_requested=False,
|
||||
originally_running=originally_running,
|
||||
)
|
||||
)
|
||||
reasons.append('scheduled update timed out')
|
||||
return finish('failure', target, targets)
|
||||
except Exception as exc:
|
||||
@@ -22131,7 +22342,17 @@ def _scheduler_loop():
|
||||
actual_target = outcome.get('target') or 'both'
|
||||
reason = outcome.get('reason')
|
||||
try:
|
||||
lxc_apps.record_schedule_run(_vmid, status, actual_target, reason)
|
||||
lxc_apps.record_schedule_run(
|
||||
_vmid,
|
||||
status,
|
||||
actual_target,
|
||||
reason,
|
||||
log_name=outcome.get('log_name'),
|
||||
reboot_required=outcome.get('reboot_required'),
|
||||
reboot_packages=outcome.get('reboot_packages'),
|
||||
)
|
||||
_vm_cache_invalidate(_vmid, _vm_schedule_cache)
|
||||
_publish_guest_modal_cache_revision(_vmid)
|
||||
except Exception as e:
|
||||
print(f"[ProxMenux] scheduler: could not record run for {_vmid}: {e}")
|
||||
print(f"[ProxMenux] scheduler: CT {_vmid} finished with status={status}"
|
||||
|
||||
@@ -3018,7 +3018,16 @@ def scheduled_app_release_gate(
|
||||
return {"allowed": True, "status": "ready", "reason": None}
|
||||
|
||||
|
||||
def record_schedule_run(vmid, status: str, target: str, reason: Optional[str] = None) -> bool:
|
||||
def record_schedule_run(
|
||||
vmid,
|
||||
status: str,
|
||||
target: str,
|
||||
reason: Optional[str] = None,
|
||||
*,
|
||||
log_name: Optional[str] = None,
|
||||
reboot_required: Optional[bool] = None,
|
||||
reboot_packages: Optional[list[str]] = None,
|
||||
) -> bool:
|
||||
"""Called by the scheduler after a fired run completes. Updates
|
||||
the schedule with last_run_at + last_run_status so the UI can show
|
||||
the outcome. `status` is one of "success" | "failure" |
|
||||
@@ -3034,6 +3043,38 @@ def record_schedule_run(vmid, status: str, target: str, reason: Optional[str] =
|
||||
sidecar["schedule"]["last_run_reason"] = str(reason)[:300]
|
||||
else:
|
||||
sidecar["schedule"].pop("last_run_reason", None)
|
||||
if log_name:
|
||||
sidecar["schedule"]["last_run_log"] = os.path.basename(str(log_name))[:220]
|
||||
else:
|
||||
sidecar["schedule"].pop("last_run_log", None)
|
||||
if reboot_required is None:
|
||||
sidecar["schedule"].pop("last_run_reboot_required", None)
|
||||
else:
|
||||
sidecar["schedule"]["last_run_reboot_required"] = bool(reboot_required)
|
||||
packages = [
|
||||
str(package).strip()[:160]
|
||||
for package in (reboot_packages or [])[:32]
|
||||
if str(package).strip()
|
||||
]
|
||||
if reboot_required and packages:
|
||||
sidecar["schedule"]["last_run_reboot_packages"] = packages
|
||||
else:
|
||||
sidecar["schedule"].pop("last_run_reboot_packages", None)
|
||||
sidecar["updated_at"] = _now_iso()
|
||||
return _write_sidecar(vmid, sidecar)
|
||||
|
||||
|
||||
def clear_schedule_reboot_required(vmid) -> bool:
|
||||
"""Clear a persisted reboot warning after the CT starts or reboots."""
|
||||
with _cache_lock:
|
||||
sidecar = _read_sidecar(vmid)
|
||||
schedule = (sidecar or {}).get("schedule")
|
||||
if not isinstance(schedule, dict):
|
||||
return False
|
||||
if schedule.get("last_run_reboot_required") is not True:
|
||||
return True
|
||||
schedule["last_run_reboot_required"] = False
|
||||
schedule.pop("last_run_reboot_packages", None)
|
||||
sidecar["updated_at"] = _now_iso()
|
||||
return _write_sidecar(vmid, sidecar)
|
||||
|
||||
|
||||
@@ -361,11 +361,47 @@ def get_available_storages() -> List[Dict[str, Any]]:
|
||||
return storages
|
||||
|
||||
|
||||
def _host_arch() -> str:
|
||||
"""Return the host's dpkg architecture (`amd64`, `arm64`, ...).
|
||||
|
||||
Falls back to mapping `uname -m` when `dpkg --print-architecture` is
|
||||
unavailable — the Alpine LXC template filenames use the dpkg style
|
||||
(`amd64`, `arm64`), so `uname -m`'s `x86_64` / `aarch64` gets
|
||||
translated to that form.
|
||||
"""
|
||||
try:
|
||||
rc, out, _ = _run_pve_cmd(["dpkg", "--print-architecture"], timeout=5)
|
||||
arch = out.strip()
|
||||
if rc == 0 and arch:
|
||||
return arch
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
machine = os.uname().machine.lower()
|
||||
except Exception:
|
||||
machine = ''
|
||||
return {
|
||||
'x86_64': 'amd64', 'amd64': 'amd64',
|
||||
'aarch64': 'arm64', 'arm64': 'arm64',
|
||||
'armv7l': 'armhf', 'armhf': 'armhf',
|
||||
'i686': 'i386', 'i386': 'i386',
|
||||
}.get(machine, 'amd64')
|
||||
|
||||
|
||||
def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
|
||||
"""Download the latest Alpine LXC template using pveam."""
|
||||
"""Download the latest Alpine LXC template using pveam.
|
||||
|
||||
Filters by the host's architecture so an x86_64 host does not end up
|
||||
with an arm64 template — the previous naive `for line in out` loop
|
||||
kept the last match and could pick any arch that `pveam available`
|
||||
happened to list (issue #324).
|
||||
"""
|
||||
print("[*] Downloading Alpine Linux template...")
|
||||
logger.info("Downloading Alpine template via pveam")
|
||||
|
||||
host_arch = _host_arch()
|
||||
logger.info(f"Host architecture detected: {host_arch}")
|
||||
|
||||
# Update template list first
|
||||
rc, out, err = _run_pve_cmd(["pveam", "update"], timeout=60)
|
||||
if rc != 0:
|
||||
@@ -377,16 +413,21 @@ def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
|
||||
logger.error(f"Failed to list available templates: {err}")
|
||||
return False
|
||||
|
||||
# Find latest Alpine template
|
||||
# Find latest Alpine template FOR THIS HOST'S ARCH. Template names
|
||||
# follow `alpine-<ver>-default_<date>_<arch>.tar.xz`; we require the
|
||||
# arch token to match the host before considering the candidate.
|
||||
alpine_template = None
|
||||
arch_token = f"_{host_arch}."
|
||||
for line in out.strip().split('\n'):
|
||||
if 'alpine-' in line.lower():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
alpine_template = parts[1] # Template name is usually second column
|
||||
low = line.lower()
|
||||
if 'alpine-' not in low or arch_token not in low:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
alpine_template = parts[1] # Template name is usually second column
|
||||
|
||||
if not alpine_template:
|
||||
logger.error("No Alpine template found in available templates")
|
||||
logger.error(f"No Alpine template found for architecture {host_arch}")
|
||||
return False
|
||||
|
||||
# Download the template
|
||||
@@ -410,10 +451,21 @@ def _find_alpine_template(storage: str = DEFAULT_STORAGE, auto_download: bool =
|
||||
if rc == 0 and out.strip():
|
||||
template_dir = os.path.dirname(out.strip())
|
||||
|
||||
# Look for Alpine templates
|
||||
# Look for Alpine templates matching the host architecture. Without
|
||||
# this filter, a host that happened to have an arm64 Alpine template
|
||||
# sitting in its cache from a previous experiment would be handed
|
||||
# that template — resulting in `arch: arm64` on the container and
|
||||
# the `Exec format error` from issue #324.
|
||||
host_arch = _host_arch()
|
||||
arch_token = f"_{host_arch}."
|
||||
try:
|
||||
templates = os.listdir(template_dir)
|
||||
alpine_templates = [t for t in templates if t.startswith("alpine-") and t.endswith((".tar.xz", ".tar.gz", ".tar"))]
|
||||
alpine_templates = [
|
||||
t for t in templates
|
||||
if t.startswith("alpine-")
|
||||
and t.endswith((".tar.xz", ".tar.gz", ".tar"))
|
||||
and arch_token in t.lower()
|
||||
]
|
||||
|
||||
if alpine_templates:
|
||||
# Sort to get latest version
|
||||
@@ -882,6 +934,7 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
||||
|
||||
pct_cmd = [
|
||||
"pct", "create", str(vmid), template,
|
||||
"--arch", _host_arch(),
|
||||
"--hostname", hostname,
|
||||
"--memory", str(container_def.get("memory", 512)),
|
||||
"--cores", str(container_def.get("cores", 1)),
|
||||
|
||||
@@ -1,4 +1,77 @@
|
||||
|
||||
## 2026-09-02
|
||||
|
||||
### New version ProxMenux v1.2.6
|
||||
|
||||
A focused release that restores AI Assistant support for OpenAI-compatible endpoints hosted on private IPs, loopback and Docker networks, aligns the Secure Gateway wizard with the host's real architecture, and consolidates several improvements landing on develop: atomic notification delivery, custom SSH ports for Borg remote targets, an optional GitHub API token for app version tracking, and richer replication failure notifications.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 AI Assistant custom OpenAI endpoint — LAN / Docker / localhost URLs
|
||||
|
||||
- Custom OpenAI-compatible endpoints reachable on private IPs, loopback or Docker networks (LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute, self-hosted proxies…) are now accepted by the Notifications API when loading the model catalogue and validating the AI configuration.
|
||||
- The dropdown surfaces the reason returned by the server (or the underlying network error) directly under the *Load* button, so misconfigurations are visible instead of silent.
|
||||
- Translated into every Monitor language.
|
||||
|
||||
Reported in [#325](https://github.com/MacRimi/ProxMenux/issues/325) by [@jorgeffonte](https://github.com/jorgeffonte).
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Secure Gateway wizard — LXC template matches host architecture
|
||||
|
||||
- Alpine template download filters `pveam available` results by the host's architecture (via `dpkg --print-architecture`, falling back to `uname -m`), so an x86_64 Proxmox host receives the `amd64` template and an arm64 host receives the `arm64` template.
|
||||
- Local template selection applies the same architecture filter when reusing a previously downloaded Alpine template.
|
||||
- `pct create` is invoked with an explicit `--arch <host>` so the container metadata matches the host's real architecture.
|
||||
|
||||
Reported in [#324](https://github.com/MacRimi/ProxMenux/issues/324) by [@N0X4DD0](https://github.com/N0X4DD0).
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Atomic notification delivery
|
||||
|
||||
- Notification events reserve their deduplication fingerprint atomically before AI processing and channel delivery, so concurrent collectors, completion callbacks or accidental parallel Monitor processes cannot send the same event twice.
|
||||
- The reservation is shared through SQLite, expires safely if an execution is interrupted and is released when no channel succeeds, preserving retries after temporary transport failures.
|
||||
|
||||
---
|
||||
|
||||
## 🗄 Borg remote target — custom SSH port
|
||||
|
||||
- The *Add Borg destination* dialog in the Monitor and the shell TUI (`menu` → *Host Backup* → *New Borg target*) accept a custom SSH port. The default stays at `22`; any value between 1 and 65535 is embedded in the persisted `ssh://user@host:port/path` URL.
|
||||
- `BORG_RSH` honours the custom port at backup time, so scheduled jobs and manual runs reach the correct port.
|
||||
- The auto key install flow (`generate-auto`) targets the custom port too.
|
||||
- Fully backwards compatible with existing `borg-targets.txt` entries created without an explicit port.
|
||||
- Capacity probes over SSH also honour the custom port, so the *Available* badge stays accurate on non-standard ports.
|
||||
|
||||
Reported in [discussion #236](https://github.com/MacRimi/ProxMenux/discussions/236) by [@songochain](https://github.com/songochain).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 App version tracking — optional GitHub API token
|
||||
|
||||
- **Settings → GitHub API** accepts an optional personal access token for release and tag checks when GitHub's anonymous quota is exhausted.
|
||||
- The token is encrypted at rest, is never returned to the browser and can be replaced or removed independently of the Notifications service.
|
||||
- The anonymous GitHub flow remains the default; a token is not required while the shared quota is available.
|
||||
- The rate-limit error points to the actual setting and is translated in every Monitor language.
|
||||
|
||||
Reported in [discussion #306](https://github.com/MacRimi/ProxMenux/discussions/306) by [@SystemIdleProcess](https://github.com/SystemIdleProcess).
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Replication failure notifications — complete job context
|
||||
|
||||
- Native Proxmox replication webhooks resolve the replication job ID, affected VM/LXC ID and guest name before rendering the notification.
|
||||
- The exact error block supplied by Proxmox is preserved as the reason, including multiline failures, with the complete message retained as a safe fallback when the block is absent.
|
||||
- Replication notifications are identified by their complete job ID, keeping failures from different replication jobs independent during deduplication.
|
||||
|
||||
Reported by Ale R.
|
||||
|
||||
---
|
||||
|
||||
For the full history of changes, see [Releases](https://github.com/MacRimi/ProxMenux/releases).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 2026-09-01
|
||||
|
||||
### New version ProxMenux v1.2.5
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.5.0
|
||||
1.2.6
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.5
|
||||
1.2.6
|
||||
|
||||
@@ -1,3 +1,76 @@
|
||||
## 2026-09-02
|
||||
|
||||
### Nueva versión ProxMenux v1.2.6
|
||||
|
||||
Una versión centrada en restaurar el soporte del Asistente IA para endpoints compatibles con OpenAI alojados en IPs privadas, loopback y redes Docker, alinear el asistente de Secure Gateway con la arquitectura real del host, y consolidar varias mejoras que ya venían acumulándose en develop: entrega atómica de notificaciones, puerto SSH personalizado para destinos remotos Borg, token opcional de la API de GitHub para el seguimiento de versiones de aplicaciones y notificaciones de fallo de replicación con contexto completo.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Endpoint OpenAI personalizado del Asistente IA — URLs de LAN / Docker / localhost
|
||||
|
||||
- Los endpoints compatibles con OpenAI accesibles en IPs privadas, loopback o redes Docker (LiteLLM, LM Studio, LocalAI, vLLM, OmniRoute, proxies autoalojados…) se aceptan al cargar el catálogo de modelos y al validar la configuración de IA.
|
||||
- El desplegable muestra el motivo devuelto por el servidor (o el error de red subyacente) justo debajo del botón *Cargar*, así una configuración incorrecta deja de aparecer como una lista vacía y silenciosa.
|
||||
- Traducido a todos los idiomas del Monitor.
|
||||
|
||||
Reportado en la [issue #325](https://github.com/MacRimi/ProxMenux/issues/325) por [@jorgeffonte](https://github.com/jorgeffonte).
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Asistente Secure Gateway — la plantilla LXC coincide con la arquitectura del host
|
||||
|
||||
- La descarga de la plantilla Alpine filtra los resultados de `pveam available` por la arquitectura del host (mediante `dpkg --print-architecture`, con fallback a `uname -m`), así un host Proxmox x86_64 recibe la plantilla `amd64` y un host arm64 recibe la plantilla `arm64`.
|
||||
- La selección de plantilla local aplica el mismo filtro de arquitectura al reutilizar una plantilla Alpine ya descargada.
|
||||
- `pct create` se invoca con `--arch <host>` explícito para que los metadatos del contenedor reflejen la arquitectura real del host.
|
||||
|
||||
Reportado en la [issue #324](https://github.com/MacRimi/ProxMenux/issues/324) por [@N0X4DD0](https://github.com/N0X4DD0).
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Entrega atómica de notificaciones
|
||||
|
||||
- Los eventos de notificación reservan su huella de deduplicación de forma atómica antes del procesado por IA y del envío por canal, de modo que colectores concurrentes, callbacks de finalización o procesos Monitor paralelos accidentales no pueden enviar el mismo evento dos veces.
|
||||
- La reserva se comparte a través de SQLite, expira de forma segura si una ejecución se interrumpe y se libera cuando ningún canal tiene éxito, preservando los reintentos ante fallos transitorios de transporte.
|
||||
|
||||
---
|
||||
|
||||
## 🗄 Destino remoto Borg — puerto SSH personalizado
|
||||
|
||||
- El diálogo *Añadir destino Borg* del Monitor y el TUI del shell (`menu` → *Host Backup* → *New Borg target*) aceptan un puerto SSH personalizado. El valor por defecto sigue siendo `22`; cualquier valor entre 1 y 65535 se incrusta en la URL `ssh://user@host:port/path` persistida.
|
||||
- `BORG_RSH` respeta el puerto personalizado en el momento del backup, así los jobs programados y las ejecuciones manuales alcanzan el puerto correcto.
|
||||
- El flujo de instalación automática de clave (`generate-auto`) también apunta al puerto personalizado.
|
||||
- Totalmente retrocompatible con las entradas existentes en `borg-targets.txt` creadas sin puerto explícito.
|
||||
- Las sondas de capacidad sobre SSH también respetan el puerto personalizado, así la insignia *Available* permanece precisa en puertos no estándar.
|
||||
|
||||
Reportado en la [discusión #236](https://github.com/MacRimi/ProxMenux/discussions/236) por [@songochain](https://github.com/songochain).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Seguimiento de versiones de aplicaciones — token opcional de la API de GitHub
|
||||
|
||||
- **Settings → GitHub API** acepta un token de acceso personal opcional para las comprobaciones de releases y tags cuando se agota la cuota anónima de GitHub.
|
||||
- El token se guarda cifrado, nunca se devuelve al navegador y puede sustituirse o eliminarse de forma independiente del servicio de Notificaciones.
|
||||
- El flujo anónimo de GitHub sigue siendo el predeterminado; no se requiere un token mientras la cuota compartida sin autenticar esté disponible.
|
||||
- El error de límite de tasa apunta al ajuste real y está traducido en todos los idiomas del Monitor.
|
||||
|
||||
Reportado en la [discusión #306](https://github.com/MacRimi/ProxMenux/discussions/306) por [@SystemIdleProcess](https://github.com/SystemIdleProcess).
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Notificaciones de fallo de replicación — contexto completo del job
|
||||
|
||||
- Los webhooks nativos de replicación de Proxmox resuelven el ID del trabajo de replicación, el ID de la VM/LXC afectada y el nombre del guest antes de renderizar la notificación.
|
||||
- El bloque de error exacto proporcionado por Proxmox se conserva como motivo, incluyendo fallos multilínea, con el mensaje completo como fallback seguro cuando el bloque no está presente.
|
||||
- Las notificaciones de replicación se identifican por su ID de job completo, así los fallos de trabajos de replicación distintos permanecen independientes durante la deduplicación.
|
||||
|
||||
Reportado por Ale R.
|
||||
|
||||
---
|
||||
|
||||
Para el historial completo de cambios, consulta [Releases](https://github.com/MacRimi/ProxMenux/releases).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 2026-09-01
|
||||
|
||||
### Nueva versión ProxMenux v1.2.5
|
||||
|
||||
@@ -140,7 +140,10 @@
|
||||
"items": [
|
||||
"Choose a preset or cron expression, then select exact targets: OS packages, individual apps, Docker Engine, standalone Docker units or Compose service groups.",
|
||||
"A release hold applies only to selected applications with version tracking. Apps without tracking run their updater whenever their schedule is due.",
|
||||
"The last-run state distinguishes success, partial completion, failure, safety hold and a run with nothing pending.",
|
||||
"The last-run state distinguishes success, partial completion, failure, safety hold and a run with nothing pending. After the first scheduled run, <strong>Updates → Scheduled updates → View log</strong> opens the complete output captured from the updater and any child scripts it invoked.",
|
||||
"After each run, ProxMenux checks the standard Debian reboot-required marker. When a restart is needed, the Updates tab and the completion notification say so; the warning is cleared when that LXC starts or restarts.",
|
||||
"On the Proxmox host, scheduled-run logs are stored in <code>/usr/local/share/proxmenux/logs/lxc-updates/</code> using the name <code><VMID>-scheduled-<run-id>.log</code>. The latest ten logs are retained per LXC and older files are removed automatically.",
|
||||
"This retained history applies to scheduled updates. A manual update displays its output live in the Monitor execution window and does not create a scheduled-run log in that directory.",
|
||||
"External host schedules detected from Proxmox VE Helper-Scripts are shown separately so overlapping automation is visible."
|
||||
],
|
||||
"callout": "Run every selected method manually before enabling a schedule. Scheduled commands cannot answer prompts."
|
||||
@@ -150,6 +153,7 @@
|
||||
"lead": "The update is not considered finished when the terminal command merely exits.",
|
||||
"items": [
|
||||
"The same run records its final result and refreshes OS package state, registered app versions and Docker inventory as applicable.",
|
||||
"Scheduled runs retain their terminal output and report whether a restart is still required to finish applying package changes.",
|
||||
"The LXC cache is replaced with the verified post-update state, so badges and buttons do not retain the previous result.",
|
||||
"If a stopped or restored LXC starts, the existing lifecycle event refreshes that LXC again. Docker inventory waits for the daemon to become ready instead of caching an empty startup result as final.",
|
||||
"Enabled notifications are emitted from the finalized run, including partial failures and grouped Docker image results."
|
||||
@@ -179,6 +183,10 @@
|
||||
{
|
||||
"problem": "A custom command fails",
|
||||
"resolution": "Run it in the LXC terminal and review its path, dependencies, non-interactive flags and exit code."
|
||||
},
|
||||
{
|
||||
"problem": "A scheduled update says that a restart is required",
|
||||
"resolution": "Open View log to review the completed run, then restart that LXC. ProxMenux clears the warning from the existing lifecycle event after the container starts again."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -140,7 +140,10 @@
|
||||
"items": [
|
||||
"Selecciona una frecuencia o expresión cron y después objetivos exactos: paquetes del SO, apps individuales, Docker Engine, unidades Docker independientes o grupos de servicios Compose.",
|
||||
"La espera tras una versión solo se aplica a las apps seleccionadas con seguimiento. Las apps sin seguimiento ejecutan su actualizador cuando vence la programación.",
|
||||
"El estado de la última ejecución diferencia entre éxito, finalización parcial, error, retención de seguridad y ausencia de elementos pendientes.",
|
||||
"El estado de la última ejecución diferencia entre éxito, finalización parcial, error, retención de seguridad y ausencia de elementos pendientes. Después de la primera ejecución programada, <strong>Actualizaciones → Actualizaciones programadas → Ver log</strong> abre la salida completa capturada del actualizador y de los scripts secundarios que haya ejecutado.",
|
||||
"Después de cada ejecución, ProxMenux comprueba el marcador estándar de Debian que indica si es necesario reiniciar. Cuando hace falta, la pestaña Actualizaciones y la notificación de finalización lo indican; el aviso se elimina cuando ese LXC se inicia o reinicia.",
|
||||
"En el host Proxmox, los logs de las ejecuciones programadas se guardan en <code>/usr/local/share/proxmenux/logs/lxc-updates/</code> con el nombre <code><VMID>-scheduled-<id-de-ejecución>.log</code>. Se conservan los diez últimos logs de cada LXC y los archivos más antiguos se eliminan automáticamente.",
|
||||
"Este historial corresponde a las actualizaciones programadas. Una actualización manual muestra su salida en tiempo real en la ventana de ejecución del Monitor y no crea un log de ejecución programada en ese directorio.",
|
||||
"Las programaciones externas detectadas de Proxmox VE Helper-Scripts se muestran aparte para hacer visible cualquier automatización coincidente."
|
||||
],
|
||||
"callout": "Cada método seleccionado debe probarse manualmente antes de programarlo. Una tarea programada no puede responder a preguntas interactivas."
|
||||
@@ -150,6 +153,7 @@
|
||||
"lead": "La actualización no se considera terminada únicamente porque el comando del terminal haya finalizado.",
|
||||
"items": [
|
||||
"La misma ejecución guarda el resultado final y actualiza, según corresponda, los paquetes del SO, las versiones de las apps y el inventario Docker.",
|
||||
"Las ejecuciones programadas conservan la salida del terminal e indican si todavía es necesario reiniciar para terminar de aplicar los cambios de los paquetes.",
|
||||
"La caché del LXC se reemplaza con el estado verificado tras la actualización para que insignias y botones no conserven el resultado anterior.",
|
||||
"Si arranca un LXC parado o restaurado, el evento de ciclo de vida existente vuelve a actualizar ese LXC. El inventario Docker espera a que el daemon esté disponible en lugar de guardar como definitivo un resultado vacío del arranque.",
|
||||
"Las notificaciones activadas se emiten desde la ejecución finalizada e incluyen fallos parciales y resultados agrupados de imágenes Docker."
|
||||
@@ -179,6 +183,10 @@
|
||||
{
|
||||
"problem": "Falla un comando personalizado",
|
||||
"resolution": "Ejecútalo en el terminal del LXC y revisa la ruta, las dependencias, los parámetros no interactivos y el código de salida."
|
||||
},
|
||||
{
|
||||
"problem": "Una actualización programada indica que es necesario reiniciar",
|
||||
"resolution": "Abre <strong>Ver log</strong> para revisar la ejecución completada y reinicia ese LXC. ProxMenux elimina el aviso mediante el evento de ciclo de vida existente cuando el contenedor vuelve a arrancar."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user