Compare commits

..

No commits in common. "3cd491eaf69d3415e9fd070fec7ff08bc4a458d5" and "ba8693d512f3c9c237ce902f88f7714e5fe6e8b4" have entirely different histories.

5 changed files with 36 additions and 96 deletions

View File

@ -1 +1 @@
v20260112-15-vspc-scroll-partial-approve-objects
v20260112-14-vspc-company-mapping-require-all

View File

@ -488,33 +488,19 @@ def inbox_message_approve_vspc_companies(message_id: int):
final_map.update(provided_map)
missing_companies = [c for c in companies_present if c not in final_map]
mapped_companies = [c for c in companies_present if c in final_map]
if not mapped_companies:
# Nothing to approve yet; user must map at least one company.
if missing_companies:
# Keep message in Inbox until all companies are mapped.
missing_str = ", ".join(missing_companies[:10])
if len(missing_companies) > 10:
missing_str += f" (+{len(missing_companies) - 10} more)"
flash(
(
"Please map at least one company before approving."
+ (f" Missing: {missing_str}" if missing_str else "")
),
"danger",
)
flash(f"Please map all companies before approving. Missing: {missing_str}", "danger")
return redirect(url_for("main.inbox"))
def _is_error_status(value: str | None) -> bool:
v = (value or "").strip().lower()
return v in {"error", "failed", "critical"} or v.startswith("fail")
created_runs: list[JobRun] = []
skipped_existing = 0
first_job: Job | None = None
# Create runs for mapped companies only. If some companies remain unmapped,
# the message stays in the Inbox so the user can map the remainder later.
for company in mapped_companies:
# Create runs for all companies in the message using the resolved mapping.
for company in companies_present:
customer_id = int(final_map[company])
customer = Customer.query.get(customer_id)
if not customer:
@ -553,15 +539,10 @@ def inbox_message_approve_vspc_companies(message_id: int):
.filter(MailObject.object_name.like(f"{company} | %"))
.all()
)
saw_error = any(_is_error_status(o.status) for o in objs)
saw_warning = any((o.status or "").strip().lower() == "warning" for o in objs)
saw_error = any((o.status or "").lower() == "error" for o in objs)
saw_warning = any((o.status or "").lower() == "warning" for o in objs)
status = "Error" if saw_error else ("Warning" if saw_warning else (msg.overall_status or "Success"))
# De-duplicate: do not create multiple runs for the same (mail_message_id, job_id).
run = JobRun.query.filter(JobRun.job_id == job.id, JobRun.mail_message_id == msg.id).first()
if run:
skipped_existing += 1
else:
run = JobRun(
job_id=job.id,
mail_message_id=msg.id,
@ -574,9 +555,7 @@ def inbox_message_approve_vspc_companies(message_id: int):
db.session.add(run)
db.session.flush()
created_runs.append(run)
# Persist objects for reporting (idempotent upsert; safe to repeat).
try:
persist_objects_for_approved_run_filtered(
customer.id,
@ -592,37 +571,13 @@ def inbox_message_approve_vspc_companies(message_id: int):
f"Filtered object persistence failed for message {msg.id} (company '{company}', job {job.id}, run {run.id}): {exc}",
)
processed_total = len(created_runs) + skipped_existing
if processed_total <= 0:
created_runs.append(run)
if not created_runs:
flash("No runs could be created for this VSPC summary.", "danger")
return redirect(url_for("main.inbox"))
# Commit created runs and any job mapping updates first.
try:
db.session.commit()
except Exception as exc:
db.session.rollback()
flash("Could not approve this job due to a database error.", "danger")
_log_admin_event("inbox_approve_error", f"Failed to approve VSPC message {msg.id}: {exc}")
return redirect(url_for("main.inbox"))
if missing_companies:
# Keep message in Inbox until all companies are mapped, but keep the already
# created runs for mapped companies.
missing_str = ", ".join(missing_companies[:10])
if len(missing_companies) > 10:
missing_str += f" (+{len(missing_companies) - 10} more)"
_log_admin_event(
"inbox_approve_vspc_partial",
f"Partially approved VSPC message {msg.id}: {processed_total} run(s) processed, missing={missing_str}",
)
flash(
f"Approved {processed_total} mapped compan{'y' if processed_total == 1 else 'ies'}. Message stays in the Inbox until all companies are mapped. Missing: {missing_str}",
"warning",
)
return redirect(url_for("main.inbox"))
# All companies mapped: mark the message as approved and move it to History.
# Update mail message to reflect approval
msg.job_id = first_job.id if first_job else None
if hasattr(msg, "approved"):
msg.approved = True
@ -637,15 +592,15 @@ def inbox_message_approve_vspc_companies(message_id: int):
db.session.commit()
except Exception as exc:
db.session.rollback()
flash("Could not finalize approval due to a database error.", "danger")
_log_admin_event("inbox_approve_error", f"Failed to finalize VSPC approval for message {msg.id}: {exc}")
flash("Could not approve this job due to a database error.", "danger")
_log_admin_event("inbox_approve_error", f"Failed to approve VSPC message {msg.id}: {exc}")
return redirect(url_for("main.inbox"))
_log_admin_event(
"inbox_approve_vspc",
f"Approved VSPC message {msg.id} into {processed_total} run(s) (job_id={msg.job_id})",
f"Approved VSPC message {msg.id} into {len(created_runs)} runs (job_id={msg.job_id})",
)
flash(f"Approved VSPC summary into {processed_total} run(s).", "success")
flash(f"Approved VSPC summary into {len(created_runs)} run(s).", "success")
return redirect(url_for("main.inbox"))

View File

@ -93,8 +93,6 @@ def _parse_vspc_active_alarms_from_html(html: str) -> Tuple[List[Dict], str, Opt
colmap["alarm_name"] = i
elif c in {"n. of repeats", "n.of repeats", "repeats"}:
colmap["repeats"] = i
elif c in {"alarm details", "alarmdetails", "details"}:
colmap["alarm_details"] = i
# Basic validation: needs at least object + current state
if "object" not in colmap or "current_state" not in colmap:
@ -120,7 +118,6 @@ def _parse_vspc_active_alarms_from_html(html: str) -> Tuple[List[Dict], str, Opt
at_time = plain[colmap.get("time", -1)].strip() if colmap.get("time", -1) >= 0 and colmap.get("time", -1) < len(plain) else ""
alarm_name = plain[colmap.get("alarm_name", -1)].strip() if colmap.get("alarm_name", -1) >= 0 and colmap.get("alarm_name", -1) < len(plain) else ""
repeats = plain[colmap.get("repeats", -1)].strip() if colmap.get("repeats", -1) >= 0 and colmap.get("repeats", -1) < len(plain) else ""
alarm_details = plain[colmap.get("alarm_details", -1)].strip() if colmap.get("alarm_details", -1) >= 0 and colmap.get("alarm_details", -1) < len(plain) else ""
state_lower = (current_state or "").lower()
status = "Success"
@ -131,13 +128,10 @@ def _parse_vspc_active_alarms_from_html(html: str) -> Tuple[List[Dict], str, Opt
status = "Warning"
saw_warning = True
# Prefer the explicit "Alarm Details" column if present.
detail_line = alarm_details or None
# Otherwise try to find a more descriptive detail line in the company text.
# Try to find a more descriptive detail line in the company text.
detail_line = None
# Prefer lines that mention the object or alarm name and are long enough to be a real description.
needles = [n for n in [obj_name, alarm_name] if n]
if not detail_line:
for ln in seg_lines:
if len(ln) < 25:
continue

View File

@ -227,7 +227,7 @@
{% endfor %}
</datalist>
<div class="table-responsive" style="max-height:55vh; overflow-y:auto;">
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>

View File

@ -220,15 +220,6 @@
- Added default customer prefill for each VSPC company based on existing per-company jobs (previous mappings).
- Changed VSPC approval flow to require all companies in the email to be mapped before approval; when mappings are incomplete the message stays in the Inbox while newly provided mappings are still saved.
---
## v20260112-15-vspc-scroll-partial-approve-objects
- Added a vertical scrollbar to the VSPC company mapping popup so “Approve mapped companies” stays reachable without browser zoom.
- Changed VSPC approval to approve only mapped companies; if some companies are still unmapped, the email stays in the Inbox while runs for mapped companies are created.
- Added de-duplication for VSPC approvals to prevent creating duplicate runs on repeated approvals.
- Improved VSPC error detection by treating both “Error” and “Failed” states as errors for the run status.
- Enhanced VSPC parsing to use the “Alarm Details” column so object rows (e.g., HV01, USB Disk) persist with their full alarm messages and become visible in the customer job view.
================================================================================================================================================
## 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.