diff --git a/.last-branch b/.last-branch index 96a874e..2da1383 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260113-08-vspc-object-linking +v20260113-08-vspc-object-linking-normalize diff --git a/containers/backupchecks/src/backend/app/job_matching.py b/containers/backupchecks/src/backend/app/job_matching.py index 496a718..6d95708 100644 --- a/containers/backupchecks/src/backend/app/job_matching.py +++ b/containers/backupchecks/src/backend/app/job_matching.py @@ -68,4 +68,53 @@ def find_matching_job(msg: MailMessage) -> Optional[Job]: if len(matches) == 1: return matches[0] + # Backwards-compatible matching for Veeam VSPC Active Alarms summary per-company jobs. + # Earlier versions could store company names with slightly different whitespace / HTML entities, + # while parsers store objects using a normalized company prefix. When the exact match fails, + # try a normalized company comparison so existing jobs continue to match. + try: + bsw = (backup or "").strip().lower() + bt = (btype or "").strip().lower() + jn = (job_name or "").strip() + if bsw == "veeam" and bt == "service provider console" and "|" in jn: + left, right = [p.strip() for p in jn.split("|", 1)] + if left.lower() == "active alarms summary" and right: + from .parsers.veeam import normalize_vspc_company_name # lazy import + + target_company = normalize_vspc_company_name(right) + if not target_company: + return None + + q2 = Job.query + if norm_from is None: + q2 = q2.filter(Job.from_address.is_(None)) + else: + q2 = q2.filter(Job.from_address == norm_from) + q2 = q2.filter(Job.backup_software == backup) + q2 = q2.filter(Job.backup_type == btype) + q2 = q2.filter(Job.job_name.ilike("Active alarms summary | %")) + + # Load a small set of candidates and compare the company portion. + candidates = q2.order_by(Job.updated_at.desc(), Job.id.desc()).limit(25).all() + normalized_matches: list[Job] = [] + for cand in candidates: + cand_name = (cand.job_name or "").strip() + if "|" not in cand_name: + continue + c_left, c_right = [p.strip() for p in cand_name.split("|", 1)] + if c_left.lower() != "active alarms summary" or not c_right: + continue + if normalize_vspc_company_name(c_right) == target_company: + normalized_matches.append(cand) + + if len(normalized_matches) > 1: + customer_ids = {m.customer_id for m in normalized_matches} + if len(customer_ids) == 1: + return normalized_matches[0] + return None + if len(normalized_matches) == 1: + return normalized_matches[0] + except Exception: + pass + return None diff --git a/containers/backupchecks/src/backend/app/parsers/veeam.py b/containers/backupchecks/src/backend/app/parsers/veeam.py index c3437b5..297220d 100644 --- a/containers/backupchecks/src/backend/app/parsers/veeam.py +++ b/containers/backupchecks/src/backend/app/parsers/veeam.py @@ -228,45 +228,6 @@ def _parse_vspc_active_alarms_from_html(html: str) -> Tuple[List[Dict], str, Opt return objects, overall_status, overall_message -def extract_vspc_active_alarms_companies(raw: str) -> List[str]: - """Extract company names with alarms > 0 from a VSPC Active Alarms summary body.""" - if not raw: - return [] - - txt = raw - if "<" in txt and ">" in txt: - txt = re.sub(r"<[^>]+>", " ", txt) - txt = _html.unescape(txt) - txt = txt.replace("\xa0", " ") - txt = re.sub(r"\s+", " ", txt).strip() - - seen: set[str] = set() - out: List[str] = [] - - for m in re.finditer( - r"\bCompany:\s*([^\(\r\n]+?)\s*\(\s*alarms?\s*:\s*(\d+)\s*\)", - txt, - flags=re.IGNORECASE, - ): - cname = (m.group(1) or "").strip() - cname = cname.replace("\xa0", " ") - cname = re.sub(r"\s+", " ", cname).strip() - try: - alarms = int(m.group(2)) - except Exception: - alarms = 0 - - if not cname or alarms <= 0: - continue - if cname in seen: - continue - seen.add(cname) - out.append(cname) - - return out - - - def _parse_cloud_connect_report_from_html(html: str) -> Tuple[List[Dict], str]: """Parse Veeam Cloud Connect daily report (provider) HTML. diff --git a/docs/changelog.md b/docs/changelog.md index 3f9c492..289bb54 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -62,6 +62,15 @@ - Uses case-insensitive matching for " | " mail objects. - Added best-effort retroactive processing after approving VSPC company mappings to automatically link older inbox messages that are now fully mapped. +--- + +## v20260113-08-vspc-object-linking-normalize + +- Fixed duplicate definition of the VSPC Active Alarms company extraction logic, which caused inconsistent company normalization. +- Ensured consistent company name normalization is used when creating per-company VSPC jobs and when linking objects to those jobs. +- Improved object linking for VSPC Active Alarms so real objects (e.g. HV01, USB Disk) are correctly associated with their jobs. +- Restored automatic re-linking of previously approved companies and objects for new and historical VSPC mails. +- Added backward-compatible matching to prevent existing VSPC jobs from breaking due to earlier inconsistent company naming. *** ## v0.1.20