Compare commits

...

2 Commits

4 changed files with 53 additions and 35 deletions

View File

@ -1 +1 @@
v20260112-12-vspc-company-mapping-popup-visible v20260112-13-vspc-company-mapping-popup-ui

View File

@ -4,6 +4,8 @@ from .routes_shared import _format_datetime, _log_admin_event, _send_mail_messag
from ..email_utils import extract_best_html_from_eml, is_effectively_blank_html from ..email_utils import extract_best_html_from_eml, is_effectively_blank_html
import time import time
import re
import html as _html
@main_bp.route("/inbox") @main_bp.route("/inbox")
@login_required @login_required
@ -149,34 +151,35 @@ def inbox_message_detail(message_id: int):
for obj in MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all() for obj in MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all()
] ]
# For VSPC Active Alarms summary messages, objects are typically not persisted until approval. # VSPC multi-company emails (e.g. "Active alarms summary") may not store parsed objects yet.
# To enable multi-company mapping in the Inbox, we expose the company list derived from parsing # Extract company names from the stored body so the UI can offer a dedicated mapping workflow.
# the stored HTML body (best-effort, without persisting objects). vspc_companies: list[str] = []
bsw = (getattr(msg, "backup_software", "") or "").strip()
btype = (getattr(msg, "backup_type", "") or "").strip()
jname = (getattr(msg, "job_name", "") or "").strip()
if not objects and bsw == "Veeam" and btype == "Service Provider Console" and jname == "Active alarms summary":
try: try:
from ..parsers.veeam import _parse_vspc_active_alarms_from_html bsw = (getattr(msg, "backup_software", "") or "").strip().lower()
btype = (getattr(msg, "backup_type", "") or "").strip().lower()
jname = (getattr(msg, "job_name", "") or "").strip().lower()
parsed_objs, _status, _msg = _parse_vspc_active_alarms_from_html(body_html or "") if bsw == "veeam" and btype == "service provider console" and jname == "active alarms summary":
companies: list[str] = [] raw = text_body if not _is_blank_text(text_body) else (html_body or "")
seen: set[str] = set() if raw:
for o in parsed_objs or []: txt = raw
name = str((o or {}).get("name") or "") # Best-effort HTML to text
ix = name.find(" | ") if "<" in txt and ">" in txt:
if ix > 0: txt = re.sub(r"<[^>]+>", " ", txt)
c = name[:ix].strip() txt = _html.unescape(txt)
if c and c not in seen: txt = re.sub(r"\s+", " ", txt).strip()
seen.add(c)
companies.append(c) seen = set()
if companies: for m2 in re.finditer(r"\bCompany:\s*([^\(\r\n]+?)\s*\(\s*alarms?\s*:", txt, flags=re.IGNORECASE):
meta["vspc_companies"] = companies cname = (m2.group(1) or "").strip()
if cname and cname not in seen:
seen.add(cname)
vspc_companies.append(cname)
except Exception: except Exception:
# Never fail the message detail endpoint due to best-effort parsing. vspc_companies = []
pass
return jsonify({"status": "ok", "meta": meta, "body_html": body_html, "objects": objects})
return jsonify({"status": "ok", "meta": meta, "body_html": body_html, "objects": objects, "vspc_companies": vspc_companies})
@main_bp.route("/inbox/message/<int:message_id>/eml") @main_bp.route("/inbox/message/<int:message_id>/eml")

View File

@ -509,6 +509,11 @@ function findCustomerIdByName(name) {
// reset // reset
mapBtn.classList.add("d-none"); mapBtn.classList.add("d-none");
if (approveBtn) approveBtn.classList.remove("d-none"); if (approveBtn) approveBtn.classList.remove("d-none");
var ciReset = document.getElementById("msg_customer_input");
if (ciReset) {
ciReset.removeAttribute("disabled");
ciReset.placeholder = "Select customer";
}
var bsw = String(meta.backup_software || "").trim(); var bsw = String(meta.backup_software || "").trim();
var btype = String(meta.backup_type || "").trim(); var btype = String(meta.backup_type || "").trim();
@ -518,14 +523,11 @@ function findCustomerIdByName(name) {
return; return;
} }
var companies = []; var companies = (data.vspc_companies || meta.vspc_companies || []);
if (Array.isArray(meta.vspc_companies) && meta.vspc_companies.length) { if (!Array.isArray(companies)) companies = [];
meta.vspc_companies.forEach(function (c) {
c = String(c || "").trim(); // Fallback for older stored messages where companies were embedded in object names.
if (c) companies.push(c); if (!companies.length) {
});
} else {
// fallback: derive from parsed objects (when available)
var objs = data.objects || []; var objs = data.objects || [];
var seen = {}; var seen = {};
objs.forEach(function (o) { objs.forEach(function (o) {
@ -543,6 +545,12 @@ function findCustomerIdByName(name) {
// Show mapping button; hide regular approve // Show mapping button; hide regular approve
mapBtn.classList.remove("d-none"); mapBtn.classList.remove("d-none");
if (approveBtn) approveBtn.classList.add("d-none"); if (approveBtn) approveBtn.classList.add("d-none");
var ci = document.getElementById("msg_customer_input");
if (ci) {
ci.value = "";
ci.setAttribute("disabled", "disabled");
ci.placeholder = "Use \"Map companies\"";
}
mapBtn.onclick = function () { mapBtn.onclick = function () {
var tbody = document.getElementById("vspcCompanyMapTbody"); var tbody = document.getElementById("vspcCompanyMapTbody");

View File

@ -206,6 +206,13 @@
- Exposed parsed VSPC company list via the inbox message detail endpoint. - Exposed parsed VSPC company list via the inbox message detail endpoint.
- Updated inbox popup logic to trigger the dedicated VSPC company-mapping workflow based on detected companies instead of parsed objects. - Updated inbox popup logic to trigger the dedicated VSPC company-mapping workflow based on detected companies instead of parsed objects.
---
## v20260112-13-vspc-company-mapping-popup-ui
- Added VSPC company extraction to the inbox message detail response so multi-company summaries expose a company list even when no objects are stored.
- Updated the Inbox message modal to show the “Map companies” button based on the returned VSPC company list (with fallback to legacy object-name parsing).
- Disabled the standard Customer selector when VSPC company mapping is available to avoid using the wrong approval flow.
================================================================================================================================================ ================================================================================================================================================
## v0.1.19 ## 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. 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.