v20260113-08-vspc-object-linking-normalize #114

Merged
ivooskamp merged 3 commits from v20260113-08-vspc-object-linking-normalize into main 2026-01-13 16:46:00 +01:00
4 changed files with 59 additions and 40 deletions
Showing only changes of commit fd0051cb29 - Show all commits

View File

@ -1 +1 @@
v20260113-08-vspc-object-linking v20260113-08-vspc-object-linking-normalize

View File

@ -68,4 +68,53 @@ def find_matching_job(msg: MailMessage) -> Optional[Job]:
if len(matches) == 1: if len(matches) == 1:
return matches[0] 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 return None

View File

@ -228,45 +228,6 @@ def _parse_vspc_active_alarms_from_html(html: str) -> Tuple[List[Dict], str, Opt
return objects, overall_status, overall_message 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]: def _parse_cloud_connect_report_from_html(html: str) -> Tuple[List[Dict], str]:
"""Parse Veeam Cloud Connect daily report (provider) HTML. """Parse Veeam Cloud Connect daily report (provider) HTML.

View File

@ -62,6 +62,15 @@
- Uses case-insensitive matching for "<company> | <object>" mail objects. - Uses case-insensitive matching for "<company> | <object>" mail objects.
- Added best-effort retroactive processing after approving VSPC company mappings to automatically link older inbox messages that are now fully mapped. - 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 ## v0.1.20