diff --git a/.last-branch b/.last-branch index 7b2d1b6..f28f3a0 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260209-06-synology-firmware-update-parser +v20260209-07-synology-drive-health-parser 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)