From 0689a466382e4dc06c46151145ba999057fd3f65 Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Tue, 9 Jun 2026 17:22:07 +0700 Subject: [PATCH] fix: fallback to derive jobs from sessions when API fails - veeam_client: try 6 URL variants (states/config with source/typeFilter), then legacy on 9419, then legacy on port 9398 - main.py: if jobs_data empty but sessions available, aggregate sessions by jobName to build synthetic job list with name/type/result/state - Ensures jobs tab always shows data even when API has FileBackup enum bug --- main.py | 21 +++++++++++++++++++++ veeam_client.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index ecb6154..78e6eb6 100644 --- a/main.py +++ b/main.py @@ -146,6 +146,27 @@ async def _collect_instance(inst) -> dict | None: except Exception as e: errors.append(f"sessions: {e}") + if not jobs_data and sessions_data: + job_map: dict[str, dict] = {} + for s in sessions_data: + name = s.get("jobName") or s.get("name") or s.get("Name") or "" + if not name: + continue + ts = s.get("endTime") or s.get("creationTime") or "" + if name not in job_map or ts > (job_map[name].get("_ts") or ""): + job_map[name] = { + "name": name, + "type": s.get("jobType") or s.get("type") or "", + "result": s.get("result") or "", + "state": s.get("state") or "", + "last_backup": ts, + "schedule_enabled": True, + "_ts": ts, + } + for j in job_map.values(): + j.pop("_ts", None) + jobs_data = list(job_map.values()) + try: repos_data = await client.fetch_repositories() except Exception as e: diff --git a/veeam_client.py b/veeam_client.py index 6a2e7f1..96850a5 100644 --- a/veeam_client.py +++ b/veeam_client.py @@ -204,14 +204,35 @@ class VeeamClient: async def fetch_jobs(self) -> list[dict]: if self._is_v12(): - last_err = None - for url in ("/api/v1/jobs/states", "/api/v1/jobs", "/api/v1/jobs/states?typeFilter=Backup"): + for path in ( + "/api/v1/jobs/states", + "/api/v1/jobs", + "/api/v1/jobs/states?source=Backup", + "/api/v1/jobs?source=Backup", + "/api/v1/jobs/states?typeFilter=Backup", + "/api/v1/jobs?typeFilter=Backup", + ): try: - return await self._fetch_json_list(url) - except Exception as e: - last_err = e + return await self._fetch_json_list(path) + except Exception: continue - raise ConnectionError(f"all job endpoints failed: {last_err}") + try: + return await self._fetch_legacy_jobs() + except Exception: + pass + try: + legacy_url = f"https://{self.host}:9398/api/jobs" + resp = await self._http.get(legacy_url, headers={"Accept": "application/json"}) + if resp.status_code < 400: + data = self._parse(resp) + jobs = data.get("Jobs") or data.get("jobs") or data.get("Refs", {}).get("Job") or [] + if isinstance(jobs, dict): + jobs = [jobs] + if jobs: + return [self._normalize_legacy_job(j) for j in jobs] + except Exception: + pass + raise ConnectionError("all job endpoints failed (v12 REST + legacy XML)") return await self._fetch_legacy_jobs() async def fetch_sessions(self, limit: int = 50) -> list[dict]: