From 9ac125d60ce8de713ce0c896576fbf72c84fe1e1 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 8 Jan 2026 16:31:25 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-08 16:31:25) --- .../backend/app/main/routes_reporting_api.py | 12 ++++- .../src/backend/app/main/routes_shared.py | 2 + .../src/backend/app/parsers/synology.py | 47 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_reporting_api.py b/containers/backupchecks/src/backend/app/main/routes_reporting_api.py index 8193bce..722cd7b 100644 --- a/containers/backupchecks/src/backend/app/main/routes_reporting_api.py +++ b/containers/backupchecks/src/backend/app/main/routes_reporting_api.py @@ -352,9 +352,14 @@ def build_report_job_filters_meta(): for bs, bt in rows: bs_val = (bs or "").strip() bt_val = (bt or "").strip() + # Exclude known informational types. if bt_val.lower() in info_backup_types: continue + # Synology DSM Updates are informational and should never appear in reports. + if (bs_val or "").strip().lower() == "synology" and bt_val.lower() == "updates": + continue + if bs_val: backup_softwares_set.add(bs_val) if bt_val: @@ -376,7 +381,7 @@ def build_report_job_filters_meta(): "backup_softwares": backup_softwares, "backup_types": backup_types, "by_backup_software": by_backup_software_out, - "excluded_backup_types": ["License Key"], + "excluded_backup_types": ["License Key", "Updates (Synology)"], } @@ -499,7 +504,10 @@ def api_reports_generate(report_id: int): where_customer = "" params = {"rid": report_id, "start_ts": report.period_start, "end_ts": report.period_end} # Job filters from report_config - where_filters = " AND COALESCE(j.backup_type,'') NOT ILIKE 'license key' " + where_filters = ( + " AND COALESCE(j.backup_type,'') NOT ILIKE 'license key' " + " AND NOT (LOWER(COALESCE(j.backup_software,'')) = 'synology' AND LOWER(COALESCE(j.backup_type,'')) = 'updates') " + ) rc = _safe_json_dict(getattr(report, "report_config", None)) filters = rc.get("filters") if isinstance(rc, dict) else None if isinstance(filters, dict): diff --git a/containers/backupchecks/src/backend/app/main/routes_shared.py b/containers/backupchecks/src/backend/app/main/routes_shared.py index 7a8430c..a0979e3 100644 --- a/containers/backupchecks/src/backend/app/main/routes_shared.py +++ b/containers/backupchecks/src/backend/app/main/routes_shared.py @@ -635,6 +635,8 @@ def _infer_schedule_map_from_runs(job_id: int): return schedule if bs == 'synology' and bt == 'account protection': return schedule + if bs == 'synology' and bt == 'updates': + return schedule if bs == 'syncovery' and bt == 'syncovery': return schedule except Exception: diff --git a/containers/backupchecks/src/backend/app/parsers/synology.py b/containers/backupchecks/src/backend/app/parsers/synology.py index 6b8bdaf..cd4f9b1 100644 --- a/containers/backupchecks/src/backend/app/parsers/synology.py +++ b/containers/backupchecks/src/backend/app/parsers/synology.py @@ -17,8 +17,49 @@ from ..models import MailMessage DSM_UPDATE_CANCELLED_PATTERNS = [ "Automatische update van DSM is geannuleerd", "Automatic DSM update was cancelled", + "Automatic update of DSM was cancelled", ] +_DSM_UPDATE_CANCELLED_HOST_RE = re.compile( + r"\b(?:geannuleerd\s+op|cancelled\s+on)\s+(?P[A-Za-z0-9._-]+)\b", + re.I, +) + +_DSM_UPDATE_FROM_HOST_RE = re.compile(r"\bVan\s+(?P[A-Za-z0-9._-]+)\b", re.I) + + +def _is_synology_dsm_update_cancelled(subject: str, text: str) -> bool: + haystack = f"{subject}\n{text}".lower() + return any(p.lower() in haystack for p in DSM_UPDATE_CANCELLED_PATTERNS) + + +def _parse_synology_dsm_update_cancelled(subject: str, text: str) -> Tuple[bool, Dict, List[Dict]]: + haystack = f"{subject}\n{text}" + host = "" + + m = _DSM_UPDATE_CANCELLED_HOST_RE.search(haystack) + if m: + host = (m.group("host") or "").strip() + if not host: + m = _DSM_UPDATE_FROM_HOST_RE.search(haystack) + if m: + host = (m.group("host") or "").strip() + + # Informational job: show in Run Checks, but do not participate in schedules / reporting. + result: Dict = { + "backup_software": "Synology", + "backup_type": "Updates", + "job_name": "Synology Automatic Update", + "overall_status": "Warning", + "overall_message": "Automatic DSM update cancelled" + (f" ({host})" if host else ""), + } + + objects: List[Dict] = [] + if host: + objects.append({"name": host, "status": "Warning"}) + + return True, result, objects + _BR_RE = re.compile(r"<\s*br\s*/?\s*>", re.I) _TAG_RE = re.compile(r"<[^>]+>") _WS_RE = re.compile(r"[\t\r\f\v ]+") @@ -392,6 +433,12 @@ def try_parse_synology(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]: # If html_body is empty, treat text_body as already-normalized text. text = _html_to_text(html_body) if html_body else (text_body or "") + # DSM Updates (informational; no schedule; excluded from reporting) + if _is_synology_dsm_update_cancelled(subject, text): + ok, result, objects = _parse_synology_dsm_update_cancelled(subject, text) + if ok: + return True, result, objects + # DSM Account Protection (informational; no schedule) if _is_synology_account_protection(subject, text): ok, result, objects = _parse_account_protection(subject, text)