Compare commits

..

No commits in common. "90317c804b33840d42de08afec1eef27ec6e60bb" and "066c45ab9b14b4f7b03cf96c3f242dee46fecee1" have entirely different histories.

4 changed files with 35 additions and 53 deletions

View File

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

View File

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

View File

@ -509,11 +509,6 @@ function findCustomerIdByName(name) {
// reset
mapBtn.classList.add("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 btype = String(meta.backup_type || "").trim();
@ -523,11 +518,14 @@ function findCustomerIdByName(name) {
return;
}
var companies = (data.vspc_companies || meta.vspc_companies || []);
if (!Array.isArray(companies)) companies = [];
// Fallback for older stored messages where companies were embedded in object names.
if (!companies.length) {
var companies = [];
if (Array.isArray(meta.vspc_companies) && meta.vspc_companies.length) {
meta.vspc_companies.forEach(function (c) {
c = String(c || "").trim();
if (c) companies.push(c);
});
} else {
// fallback: derive from parsed objects (when available)
var objs = data.objects || [];
var seen = {};
objs.forEach(function (o) {
@ -545,12 +543,6 @@ function findCustomerIdByName(name) {
// Show mapping button; hide regular approve
mapBtn.classList.remove("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 () {
var tbody = document.getElementById("vspcCompanyMapTbody");

View File

@ -206,13 +206,6 @@
- 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.
---
## 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
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.