diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index 7dee8a4..ac2e4a8 100644 --- a/app/modules/documents/agent_package.py +++ b/app/modules/documents/agent_package.py @@ -4,7 +4,7 @@ import io from pathlib import Path import zipfile -ERP_LOCAL_AGENT_VERSION = "1.26.23" +ERP_LOCAL_AGENT_VERSION = "1.26.24" ERP_LOCAL_AGENT_NAME = "ERP Local Agent" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" _DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) diff --git a/app/modules/documents/local_agent_runtime/ERPAgentSupervisor.pyw b/app/modules/documents/local_agent_runtime/ERPAgentSupervisor.pyw index 57ebb66..65888c6 100644 --- a/app/modules/documents/local_agent_runtime/ERPAgentSupervisor.pyw +++ b/app/modules/documents/local_agent_runtime/ERPAgentSupervisor.pyw @@ -23,6 +23,7 @@ STATE_FILE = DATA_DIR / "supervisor_state.json" OWNER_FILE = DATA_DIR / "supervisor_owner.json" REQUEST_FILE = UPDATES_DIR / "supervisor_request.json" UPDATE_PROGRESS_FILE = DATA_DIR / "update_progress.json" +FAILED_UPDATES_FILE = DATA_DIR / "failed_updates.json" WORKER_LOG = LOG_DIR / "worker-supervisor.log" SUPERVISOR_LOG = LOG_DIR / "supervisor.log" DASHBOARD_URL = "http://127.0.0.1:8788" @@ -110,6 +111,36 @@ def _update_progress(phase: str, percent: int, message: str, *, version: str = " _write_json_atomic(UPDATE_PROGRESS_FILE, payload) +def _read_failed_updates() -> dict: + try: + if FAILED_UPDATES_FILE.exists(): + value = json.loads(FAILED_UPDATES_FILE.read_text(encoding="utf-8")) + return value if isinstance(value, dict) else {} + except Exception: + pass + return {} + + +def _mark_failed_update(version: str, error: str) -> None: + if not version: + return + payload = _read_failed_updates() + previous = payload.get(version) if isinstance(payload.get(version), dict) else {} + payload[version] = { + "failed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "error": str(error)[:2000], + "attempts": int(previous.get("attempts") or 0) + 1, + } + _write_json_atomic(FAILED_UPDATES_FILE, payload) + + +def _clear_failed_update(version: str) -> None: + payload = _read_failed_updates() + if version in payload: + payload.pop(version, None) + _write_json_atomic(FAILED_UPDATES_FILE, payload) + + def _dashboard_ready(timeout: float = 1.5) -> bool: try: with urllib.request.urlopen(DASHBOARD_URL + "/api/status", timeout=timeout) as response: @@ -443,6 +474,7 @@ class Supervisor: raise RuntimeError(f"Updated worker exited with code {self.worker.returncode}.") if _dashboard_ready(): _update_progress("complete", 100, f"ERP Local Agent {version} updated successfully.", version=version, status="updated") + _clear_failed_update(version) _state( "updated", f"ERP Local Agent {version} installed and restarted successfully.", @@ -456,6 +488,7 @@ class Supervisor: time.sleep(1) raise RuntimeError("Updated worker did not become healthy within the restart timeout.") except Exception as exc: + _mark_failed_update(version, str(exc)) _update_progress("rollback", 92, f"Update failed; restoring the previous runtime: {exc}", version=version, status="rollback") _state("rollback", f"Update {version} failed. Restoring previous worker.", target_version=version) self.stop_worker() diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py index 3349620..c477663 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.26.23" +__version__ = "1.26.24" AGENT_NAME = "ERP Local Agent" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/client.py b/app/modules/documents/local_agent_runtime/erp_local_agent/client.py index 5c2439c..e97fc10 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/client.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/client.py @@ -105,9 +105,17 @@ class ERPClient: response = self.session.post(self._url(f"/documents/storage-agent/download-requests/{request_id}/upload"), files=files, data=data, timeout=max(self.config.request_timeout_seconds, 300)) response.raise_for_status(); return response.json() if response.content else {"status": "ok"} - def update_manifest(self) -> dict[str, Any]: - response = self.session.get(self._url("/documents/erp-local-agent/update-manifest"), timeout=self.config.request_timeout_seconds) - response.raise_for_status(); return response.json() + def update_manifest(self, *, timeout_seconds: int = 10) -> dict[str, Any]: + # Update checks are UI/background housekeeping and must never inherit the + # long general-purpose agent timeout. Keep connect/read bounds short so a + # slow or temporarily unreachable ERP cannot leave the dashboard at 10%. + timeout_seconds = max(3, min(int(timeout_seconds or 10), 15)) + response = self.session.get( + self._url("/documents/erp-local-agent/update-manifest"), + timeout=(min(5, timeout_seconds), timeout_seconds), + ) + response.raise_for_status() + return response.json() def download_update_package(self) -> bytes: response = self.session.get(self._url("/documents/erp-local-agent/update-package"), timeout=max(self.config.request_timeout_seconds, 300)) diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/dashboard.py b/app/modules/documents/local_agent_runtime/erp_local_agent/dashboard.py index 03ac1c7..0c85e6a 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/dashboard.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/dashboard.py @@ -163,7 +163,7 @@ class AgentDashboard: result=start_operator_download(token,str(dashboard.config.erp_base_url or ""),data_root) return self._json({"ok":True,**result}) if self.path == "/api/update/check": - return self._json({"ok": True, "update": dashboard.updater.check_for_update(force=True)}) + return self._json({"ok": True, "update": dashboard.updater.start_check()}) if self.path == "/api/update/download": return self._json({"ok": True, "update": dashboard.updater.start_download()}) if self.path == "/api/update/install": @@ -289,7 +289,7 @@ if(installWatching&&installTarget&&s.agent.version===installTarget){setProgress( await refreshHistory();await refreshMirrorProgress();}catch(e){if(installWatching){offlineTicks++;let estimated=Math.min(92,Math.max(lastServerPercent,35+offlineTicks*3));setProgress('Applying Update',estimated,'Local Agent is restarting. Waiting for the updated dashboard to come back online…',true)}else{console.log(e)}}} async function refreshHistory(){try{const r=await fetch('/api/history',{cache:'no-store'});const h=await r.json();let rows=h.events.slice(0,12).map(x=>'
| Time UTC | Level | Event | Message |
|---|