v20260113-08-vspc-object-linking-normalize #114
@ -1 +1 @@
|
|||||||
v20260113-08-vspc-object-linking
|
v20260113-08-vspc-object-linking-normalize
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user