From f539d62dafccb9e91bdb02f1f3eb3f030d562b76 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 17:02:08 +0100 Subject: [PATCH] Add Synology monthly drive health report parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parser for Synology monthly drive health reports with support for both Dutch and English notifications. Reports are classified as informational and excluded from schedule learning and reporting logic. Features: - Recognizes Dutch ("Maandelijks schijfintegriteitsrapport", "Gezond") and English ("Monthly Drive Health Report", "Healthy") variants - Extracts hostname from subject or body ("Van/From NAS-HOSTNAME") - Automatic status detection: Healthy/Gezond/No problem detected → Success, otherwise → Warning - Backup type: "Health Report", Job name: "Monthly Drive Health" - Added registry entry (order 237) for /parsers page visibility Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/parsers/registry.py | 26 +++++++ .../src/backend/app/parsers/synology.py | 75 +++++++++++++++++++ docs/changelog-claude.md | 1 + 3 files changed, 102 insertions(+) diff --git a/containers/backupchecks/src/backend/app/parsers/registry.py b/containers/backupchecks/src/backend/app/parsers/registry.py index 1ea80b4..1cbfc29 100644 --- a/containers/backupchecks/src/backend/app/parsers/registry.py +++ b/containers/backupchecks/src/backend/app/parsers/registry.py @@ -113,6 +113,32 @@ PARSER_DEFINITIONS = [ }, }, }, + { + "name": "synology_drive_health", + "backup_software": "Synology", + "backup_types": ["Health Report"], + "order": 237, + "enabled": True, + "match": { + "subject_contains_any": ["schijfintegriteitsrapport", "Drive Health Report"], + "body_contains_any": ["health of the drives", "integriteitsrapport van de schijven"], + }, + "description": "Parses Synology monthly drive health reports (informational; excluded from reporting and missing logic).", + "example": { + "subject": "[NAS-HOSTNAME] Monthly Drive Health Report on NAS-HOSTNAME - Healthy", + "from_address": "nas@example.local", + "body_snippet": "The following is your monthly report regarding the health of the drives on NAS-HOSTNAME. No problem detected with the drives in DSM.", + "parsed_result": { + "backup_software": "Synology", + "backup_type": "Health Report", + "job_name": "Monthly Drive Health", + "overall_status": "Success", + "objects": [ + {"name": "NAS-HOSTNAME", "status": "Success"} + ], + }, + }, + }, { "name": "veeam_replication_job", "backup_software": "Veeam", diff --git a/containers/backupchecks/src/backend/app/parsers/synology.py b/containers/backupchecks/src/backend/app/parsers/synology.py index 3164633..192a642 100644 --- a/containers/backupchecks/src/backend/app/parsers/synology.py +++ b/containers/backupchecks/src/backend/app/parsers/synology.py @@ -73,6 +73,75 @@ def _parse_synology_dsm_update_cancelled(subject: str, text: str) -> Tuple[bool, return True, result, objects + +# --- Synology Drive Health Report (informational, excluded from reporting) --- + +DRIVE_HEALTH_PATTERNS = [ + "schijfintegriteitsrapport", + "Drive Health Report", + "Monthly Drive Health", + "health of the drives", + "integriteitsrapport van de schijven", +] + +_DRIVE_HEALTH_SUBJECT_RE = re.compile( + r"\b(?:schijfintegriteitsrapport\s+over|Drive\s+Health\s+Report\s+on)\s+(?P[A-Za-z0-9._-]+)", + re.I, +) + +_DRIVE_HEALTH_FROM_RE = re.compile(r"\b(?:Van|From)\s+(?P[A-Za-z0-9._-]+)\b", re.I) + +_DRIVE_HEALTH_STATUS_HEALTHY_RE = re.compile( + r"\b(?:Gezond|Healthy|geen\s+problemen\s+gedetecteerd|No\s+problem\s+detected)\b", + re.I, +) + + +def _is_synology_drive_health(subject: str, text: str) -> bool: + haystack = f"{subject}\n{text}".lower() + return any(p.lower() in haystack for p in DRIVE_HEALTH_PATTERNS) + + +def _parse_synology_drive_health(subject: str, text: str) -> Tuple[bool, Dict, List[Dict]]: + haystack = f"{subject}\n{text}" + host = "" + + # Try to extract hostname from subject first + m = _DRIVE_HEALTH_SUBJECT_RE.search(subject or "") + if m: + host = (m.group("host") or "").strip() + + # Fallback: extract from body "Van/From NAS-NAME" + if not host: + m = _DRIVE_HEALTH_FROM_RE.search(text or "") + if m: + host = (m.group("host") or "").strip() + + # Determine status based on health indicators + overall_status = "Success" + overall_message = "Healthy" + + if not _DRIVE_HEALTH_STATUS_HEALTHY_RE.search(haystack): + # If we don't find healthy indicators, mark as Warning + overall_status = "Warning" + overall_message = "Drive health issue detected" + + # Informational job: show in Run Checks, but do not participate in schedules / reporting. + result: Dict = { + "backup_software": "Synology", + "backup_type": "Health Report", + "job_name": "Monthly Drive Health", + "overall_status": overall_status, + "overall_message": overall_message + (f" ({host})" if host else ""), + } + + objects: List[Dict] = [] + if host: + objects.append({"name": host, "status": overall_status}) + + 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 ]+") @@ -509,6 +578,12 @@ def try_parse_synology(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]: if ok: return True, result, objects + # Drive Health Report (informational; no schedule; excluded from reporting) + if _is_synology_drive_health(subject, text): + ok, result, objects = _parse_synology_drive_health(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) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index f5a116d..737a6f0 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -5,6 +5,7 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-09] ### Added +- Added parser for Synology monthly drive health reports (backup software: Synology, backup type: Health Report, job name: Monthly Drive Health, informational only, no schedule learning) with support for both Dutch and English notifications ("schijfintegriteitsrapport"/"Drive Health Report") and automatic status detection (Healthy/Gezond → Success, problems → Warning) - Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) - Added "Preview orphaned jobs" button to show detailed list of jobs to be deleted with run/email counts before confirming deletion (verification step for safety) - Added "Generate test emails" feature in Settings → Maintenance with three separate buttons to create fixed test email sets (success/warning/error) in inbox for testing parsers and maintenance operations (each set contains exactly 3 Veeam Backup Job emails with the same job name "Test-Backup-Job" and different dates/objects/statuses for reproducible testing and proper status flow testing)