Compare commits

..

No commits in common. "14d2422a1fbaa03a8dc820db1c4631e8e1f8e13e" and "b610ab511deffbd8cb2d0dd7b12fee85bbdc3e7b" have entirely different histories.

7 changed files with 2 additions and 156 deletions

View File

@ -1 +1 @@
v20260112-05-qnap-firmware-update-info-parser
v20260112-04-remove-runchecks-success-override-button

View File

@ -360,10 +360,6 @@ def build_report_job_filters_meta():
if (bs_val or "").strip().lower() == "synology" and bt_val.lower() == "updates":
continue
# QNAP firmware update notifications are informational and should never appear in reports.
if (bs_val or "").strip().lower() == "qnap" and bt_val.lower() == "firmware update":
continue
if bs_val:
backup_softwares_set.add(bs_val)
if bt_val:
@ -385,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", "Updates (Synology)", "Firmware Update (QNAP)"],
"excluded_backup_types": ["License Key", "Updates (Synology)"],
}
@ -511,7 +507,6 @@ def api_reports_generate(report_id: int):
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') "
" AND NOT (LOWER(COALESCE(j.backup_software,'')) = 'qnap' AND LOWER(COALESCE(j.backup_type,'')) = 'firmware update') "
)
rc = _safe_json_dict(getattr(report, "report_config", None))
filters = rc.get("filters") if isinstance(rc, dict) else None

View File

@ -637,8 +637,6 @@ def _infer_schedule_map_from_runs(job_id: int):
return schedule
if bs == 'synology' and bt == 'updates':
return schedule
if bs == 'qnap' and bt == 'firmware update':
return schedule
if bs == 'syncovery' and bt == 'syncovery':
return schedule
except Exception:

View File

@ -14,7 +14,6 @@ from .veeam import try_parse_veeam
from .rdrive import try_parse_rdrive
from .syncovery import try_parse_syncovery
from .ntfs_auditing import try_parse_ntfs_auditing
from .qnap import try_parse_qnap
def _sanitize_text(value: object) -> object:
@ -107,8 +106,6 @@ def parse_mail_message(msg: MailMessage) -> None:
try:
handled, result, objects = try_parse_3cx(msg)
if not handled:
handled, result, objects = try_parse_qnap(msg)
if not handled:
handled, result, objects = try_parse_synology(msg)
if not handled:

View File

@ -1,100 +0,0 @@
from __future__ import annotations
import html
import re
from typing import Dict, Tuple, List
from ..models import MailMessage
_SUBJECT_RE = re.compile(
r"^\[(?P<severity>info|warning|error)\]\s*\[\s*firmware\s+update\s*\]\s*notification\s+from\s+your\s+device\s*:\s*(?P<host>.+)$",
re.I,
)
_NAS_NAME_RE = re.compile(r"\bNAS\s*Name\s*:\s*(?P<host>[^\n<]+)", re.I)
_APP_NAME_RE = re.compile(r"\bApp\s*Name\s*:\s*(?P<app>[^\n<]+)", re.I)
_CATEGORY_RE = re.compile(r"\bCategory\s*:\s*(?P<cat>[^\n<]+)", re.I)
_MESSAGE_RE = re.compile(r"\bMessage\s*:\s*(?P<msg>.+)$", re.I | re.M)
_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 ]+")
def _html_to_text(value: str) -> str:
if not value:
return ""
s = value
s = _BR_RE.sub("\n", s)
s = _TAG_RE.sub("", s)
s = html.unescape(s)
s = s.replace("\u00a0", " ")
# keep newlines, but normalize whitespace on each line
lines = [(_WS_RE.sub(" ", ln)).strip() for ln in s.split("\n")]
return "\n".join([ln for ln in lines if ln]).strip()
def try_parse_qnap(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]:
"""Parse QNAP Notification Center e-mails.
Supported (informational):
- Firmware Update notifications
Subject: [Info][Firmware Update] Notification from your device: <HOST>
These notifications are informational: they should be visible in Run Checks,
but they must not participate in schedule inference, missed/expected logic,
or reporting.
"""
subject = (getattr(msg, "subject", None) or "").strip()
if not subject:
return False, {}, []
m = _SUBJECT_RE.match(subject)
if not m:
return False, {}, []
host = (m.group("host") or "").strip()
html_body = getattr(msg, "html_body", None) or ""
text_body = getattr(msg, "text_body", None) or getattr(msg, "body", None) or ""
text = _html_to_text(html_body) if html_body else (text_body or "")
if text:
m_host = _NAS_NAME_RE.search(text)
if m_host:
host = (m_host.group("host") or "").strip() or host
# Prefer the detailed 'Message:' line from the body.
overall_message = None
if text:
m_msg = _MESSAGE_RE.search(text)
if m_msg:
overall_message = (m_msg.group("msg") or "").strip() or None
# If the body doesn't contain a dedicated message line, derive one.
if not overall_message and text:
parts: List[str] = []
m_app = _APP_NAME_RE.search(text)
if m_app:
parts.append((m_app.group("app") or "").strip())
m_cat = _CATEGORY_RE.search(text)
if m_cat:
parts.append((m_cat.group("cat") or "").strip())
if parts:
overall_message = " / ".join([p for p in parts if p]) or None
result: Dict = {
"backup_software": "QNAP",
"backup_type": "Firmware Update",
"job_name": "Firmware Update",
"overall_status": "Warning",
"overall_message": overall_message,
}
objects: List[Dict] = []
if host:
objects.append({"name": host, "status": "Warning"})
return True, result, objects

View File

@ -61,32 +61,6 @@ PARSER_DEFINITIONS = [
},
},
},
{
"name": "qnap_firmware_update",
"backup_software": "QNAP",
"backup_types": ["Firmware Update"],
"order": 235,
"enabled": True,
"match": {
"from_contains": "notifications@",
"subject_contains": "Firmware Update",
},
"description": "Parses QNAP Notification Center firmware update notifications (informational; excluded from reporting and missing logic).",
"example": {
"subject": "[Info][Firmware Update] Notification from your device: BETSIES-NAS01",
"from_address": "notifications@customer.tld",
"body_snippet": "NAS Name: BETSIES-NAS01\n...\nMessage: ...",
"parsed_result": {
"backup_software": "QNAP",
"backup_type": "Firmware Update",
"job_name": "Firmware Update",
"overall_status": "Warning",
"objects": [
{"name": "BETSIES-NAS01", "status": "Warning", "error_message": None}
],
},
},
},
{
"name": "veeam_replication_job",
"backup_software": "Veeam",

View File

@ -133,24 +133,6 @@
- Set overall status to Warning when the subject indicates any detected changes (↑/↓ counts > 0); otherwise Success.
- Ensured job name is consistently generated as "<hostname> file audits" for all supported senders.
---
## v20260112-04-remove-runchecks-success-override-button
- Removed the “Mark success (override)” button from the Run Checks popup.
- Prevented creation of actual overrides when marking individual runs as success.
- Simplified override overview by ensuring Run Checks actions no longer affect override administration.
---
## v20260112-05-qnap-firmware-update-info-parser
- Added parser support for QNAP Firmware Update notifications.
- Classified firmware update messages as informational warnings.
- Excluded QNAP firmware update messages from missing-run detection logic.
- Excluded QNAP firmware update messages from reports and scheduling.
- Ensured affected NAS devices are only shown in Run Checks when the message occurs.
================================================================================================================================================
## v0.1.19
This release delivers a broad set of improvements focused on reliability, transparency, and operational control across mail processing, administrative auditing, and Run Checks workflows. The changes aim to make message handling more robust, provide better insight for administrators, and give operators clearer and more flexible control when reviewing backup runs.