diff --git a/.gitignore b/.gitignore index decc95a..aa26c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ # Claude Code confidential files .claude/ + +# Codex local workspace files +.codex/ + +# Python cache artifacts +__pycache__/ +*.pyc diff --git a/containers/backupchecks/src/backend/app/main/routes_shared.py b/containers/backupchecks/src/backend/app/main/routes_shared.py index 217d1be..1818ee3 100644 --- a/containers/backupchecks/src/backend/app/main/routes_shared.py +++ b/containers/backupchecks/src/backend/app/main/routes_shared.py @@ -679,6 +679,8 @@ def _infer_schedule_map_from_runs(job_id: int): return schedule if bs == 'qnap' and bt == 'firmware update': return schedule + if bs == '3cx' and bt == 'update': + return schedule if bs == 'syncovery' and bt == 'syncovery': return schedule except Exception: @@ -994,4 +996,3 @@ def _next_ticket_code(now_utc: datetime) -> str: seq = 1 return f"{prefix}{seq:04d}" - diff --git a/containers/backupchecks/src/backend/app/parsers/threecx.py b/containers/backupchecks/src/backend/app/parsers/threecx.py index a56ee93..9d6f56b 100644 --- a/containers/backupchecks/src/backend/app/parsers/threecx.py +++ b/containers/backupchecks/src/backend/app/parsers/threecx.py @@ -24,6 +24,10 @@ def try_parse_3cx(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]: - SSL Certificate Renewal (informational) Subject: '3CX Notification: SSL Certificate Renewal - ' Body contains an informational message about the renewal. + + - Update Successful (informational) + Subject: '3CX Notification: Update Successful - ' + Body confirms update completion and healthy services. """ subject = (msg.subject or "").strip() if not subject: @@ -38,11 +42,16 @@ def try_parse_3cx(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]: subject, flags=re.IGNORECASE, ) + m_update = re.match( + r"^3CX Notification:\s*Update Successful\s*-\s*(.+)$", + subject, + flags=re.IGNORECASE, + ) - if not m_backup and not m_ssl: + if not m_backup and not m_ssl and not m_update: return False, {}, [] - job_name = (m_backup or m_ssl).group(1).strip() + job_name = (m_backup or m_ssl or m_update).group(1).strip() body = (getattr(msg, "text_body", None) or getattr(msg, "body", None) or "") if not body: @@ -60,6 +69,17 @@ def try_parse_3cx(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]: } return True, result, [] + # Update successful: store as tracked informational run + if m_update: + result = { + "backup_software": "3CX", + "backup_type": "Update", + "job_name": job_name, + "overall_status": "Success", + "overall_message": body or None, + } + return True, result, [] + # Backup complete backup_file = None m_file = re.search(r"^\s*Backup\s+name\s*:\s*(.+?)\s*$", body, flags=re.IGNORECASE | re.MULTILINE) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 20583f0..979c844 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -12,6 +12,7 @@ This file documents all changes made to this project via Claude Code. - Approved Jobs import now only applies `autotask_company_id` and `autotask_company_name` when the import option is checked - Customers CSV import now only applies Autotask mapping fields when the import option is checked - Import success and audit output now includes whether Autotask IDs were imported +- 3CX parser now recognizes `3CX Notification: Update Successful - ` as an informational run with `backup_software: 3CX`, `backup_type: Update`, and `overall_status: Success`, and excludes this type from schedule inference (no Expected/Missed generation) ## [2026-02-16]