From ea8c29ecbdfccf90603294238c984cec3ec8d2f3 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 2 Sep 2026 22:18:52 +0200 Subject: [PATCH] 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. --- AppImage/components/notification-settings.tsx | 25 +- AppImage/components/release-notes-modal.tsx | 50 ++-- AppImage/components/virtual-machines.tsx | 186 ++++++++++---- AppImage/lib/version.ts | 2 +- AppImage/messages/de/common.json | 23 +- AppImage/messages/en/common.json | 23 +- AppImage/messages/es/common.json | 23 +- AppImage/messages/fr/common.json | 23 +- AppImage/messages/it/common.json | 23 +- AppImage/messages/pt/common.json | 23 +- AppImage/messages/sk/common.json | 23 +- AppImage/messages/sv/common.json | 23 +- AppImage/package-lock.json | 4 +- AppImage/package.json | 2 +- AppImage/scripts/flask_notification_routes.py | 18 +- AppImage/scripts/flask_server.py | 237 +++++++++++++++++- AppImage/scripts/lxc_apps.py | 43 +++- AppImage/scripts/oci_manager.py | 81 ++++-- CHANGELOG.md | 73 ++++++ beta_version.txt | 2 +- version.txt | 2 +- web/data/changelog/es.md | 73 ++++++ .../monitor/dashboard/vms-lxcs-updates.json | 10 +- .../monitor/dashboard/vms-lxcs-updates.json | 10 +- 24 files changed, 884 insertions(+), 118 deletions(-) diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index fe7b54df..ecebac0a 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -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([]) + // 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(null) const [loadingProviderModels, setLoadingProviderModels] = useState(false) const [showCustomPromptInfo, setShowCustomPromptInfo] = useState(false) const [editingCustomPrompt, setEditingCustomPrompt] = useState(false) @@ -992,11 +998,12 @@ 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", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + body: JSON.stringify({ provider, api_key: apiKey, ollama_url: config.ai_ollama_url, @@ -1009,8 +1016,8 @@ export function NotificationSettings() { updateConfig(prev => { if (!prev.ai_model || !data.models.includes(prev.ai_model)) { const modelToSelect = data.recommended || data.models[0] - return { - ...prev, + return { + ...prev, ai_model: modelToSelect, ai_models: { ...prev.ai_models, [provider]: modelToSelect } } @@ -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 && (

{t("settings.notifications.ai.modelsAvailable", { count: providerModels.length })}

)} + {/* 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 && ( +

{providerModelsError}

+ )} {/* Prompt Mode section */} diff --git a/AppImage/components/release-notes-modal.tsx b/AppImage/components/release-notes-modal.tsx index 53466ab2..f21b7cfa 100644 --- a/AppImage/components/release-notes-modal.tsx +++ b/AppImage/components/release-notes-modal.tsx @@ -18,6 +18,23 @@ interface ReleaseNote { } export const CHANGELOG: Record = { + "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 = { const CURRENT_VERSION_FEATURES = [ { icon: , - 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: , - 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: , + 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: , - 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: , + 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: , - 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: , + 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: , - 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: , + 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: , + 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.).", }, ] diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index afe0c8b4..208fd818 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -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(null) const [scheduleLastRunStatus, setScheduleLastRunStatus] = useState(null) const [scheduleLastRunReason, setScheduleLastRunReason] = useState(null) + const [scheduleLastRunLog, setScheduleLastRunLog] = useState(null) + const [scheduleLastRunRebootRequired, setScheduleLastRunRebootRequired] = useState(false) + const [scheduleLastRunRebootPackages, setScheduleLastRunRebootPackages] = useState([]) + const [scheduleLogOpen, setScheduleLogOpen] = useState(false) + const [scheduleLogLoading, setScheduleLogLoading] = useState(false) + const [scheduleLogContent, setScheduleLogContent] = useState("") + const [scheduleLogError, setScheduleLogError] = useState(null) + const [scheduleLogTruncated, setScheduleLogTruncated] = useState(false) const [scheduleSaving, setScheduleSaving] = useState(false) const [scheduleError, setScheduleError] = useState(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 ( +
+
+ {t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })} + {scheduleLastRunStatus && ( + <> · + {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")} + + )} +
+ {scheduleLastRunReason && ( +
+ {scheduleLastRunReason} +
+ )} + {scheduleLastRunRebootRequired && ( +
+ +
+
{t("vmLxc.scheduled.rebootRequired")}
+ {scheduleLastRunRebootPackages.length > 0 && ( +
+ {t("vmLxc.scheduled.rebootPackages", { packages: scheduleLastRunRebootPackages.join(", ") })} +
+ )} +
+
+ )} + {scheduleLastRunLog && ( + + )} +
+ ) + } + // 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 })} )} - {scheduleLastRunAt && ( -
- {t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })} - {scheduleLastRunStatus && ( - <> · - {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")} - - )} -
- )} - {scheduleLastRunReason && ( -
- {scheduleLastRunReason} -
- )} + {renderScheduleRunDetails(true)} )} {!optionsEditMode && !scheduleConfigured && !externalCron && ( @@ -6351,25 +6424,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { )} - {scheduleLastRunAt && ( -
- {t("vmLxc.scheduled.lastRun", { date: new Date(scheduleLastRunAt).toLocaleString() })} - {scheduleLastRunStatus && ( - <> · - {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")} - - )} - {scheduleLastRunReason &&
{scheduleLastRunReason}
} -
- )} + {renderScheduleRunDetails()} )} @@ -6996,6 +7051,51 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { + + + + + + {t("vmLxc.scheduled.logTitle")} + + + {t("vmLxc.scheduled.logDescription", { + name: selectedVM?.name || "LXC", + vmid: selectedVM?.vmid || "—", + })} + + +
+ {scheduleLogLoading ? ( +
+ + {t("vmLxc.scheduled.logLoading")} +
+ ) : scheduleLogError ? ( +
+ {scheduleLogError} +
+ ) : ( +
+ {scheduleLogTruncated && ( +
+ {t("vmLxc.scheduled.logTruncated")} +
+ )} +
+                  {scheduleLogContent || t("vmLxc.scheduled.logEmpty")}
+                
+
+ )} +
+ + + +
+
+ {/* LXC Terminal Modal */} {terminalVmid !== null && ( 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//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//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}" diff --git a/AppImage/scripts/lxc_apps.py b/AppImage/scripts/lxc_apps.py index f4531fb4..c5e5bbdb 100644 --- a/AppImage/scripts/lxc_apps.py +++ b/AppImage/scripts/lxc_apps.py @@ -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) diff --git a/AppImage/scripts/oci_manager.py b/AppImage/scripts/oci_manager.py index 3c2b0329..4d004525 100644 --- a/AppImage/scripts/oci_manager.py +++ b/AppImage/scripts/oci_manager.py @@ -361,32 +361,73 @@ 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: logger.warning(f"Failed to update template list: {err}") - + # Get available Alpine templates rc, out, err = _run_pve_cmd(["pveam", "available", "--section", "system"], timeout=30) if rc != 0: 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--default__.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,11 +451,22 @@ 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 alpine_templates.sort(reverse=True) @@ -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)), diff --git a/CHANGELOG.md b/CHANGELOG.md index bced5309..5f25a490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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 diff --git a/beta_version.txt b/beta_version.txt index 06e45e12..3c43790f 100644 --- a/beta_version.txt +++ b/beta_version.txt @@ -1 +1 @@ -1.2.5.0 +1.2.6 diff --git a/version.txt b/version.txt index c813fe11..3c43790f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.2.5 +1.2.6 diff --git a/web/data/changelog/es.md b/web/data/changelog/es.md index 34bf9996..470d775c 100644 --- a/web/data/changelog/es.md +++ b/web/data/changelog/es.md @@ -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 ` 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 diff --git a/web/messages/en/docs/monitor/dashboard/vms-lxcs-updates.json b/web/messages/en/docs/monitor/dashboard/vms-lxcs-updates.json index 9b16bb35..8b594f24 100644 --- a/web/messages/en/docs/monitor/dashboard/vms-lxcs-updates.json +++ b/web/messages/en/docs/monitor/dashboard/vms-lxcs-updates.json @@ -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, Updates → Scheduled updates → View log 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 /usr/local/share/proxmenux/logs/lxc-updates/ using the name <VMID>-scheduled-<run-id>.log. 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." } ] }, diff --git a/web/messages/es/docs/monitor/dashboard/vms-lxcs-updates.json b/web/messages/es/docs/monitor/dashboard/vms-lxcs-updates.json index 34d3562f..9047862c 100644 --- a/web/messages/es/docs/monitor/dashboard/vms-lxcs-updates.json +++ b/web/messages/es/docs/monitor/dashboard/vms-lxcs-updates.json @@ -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, Actualizaciones → Actualizaciones programadas → Ver log 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 /usr/local/share/proxmenux/logs/lxc-updates/ con el nombre <VMID>-scheduled-<id-de-ejecución>.log. 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 Ver log 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." } ] },