diff --git a/containers/backupchecks/src/backend/app/main/routes_inbox.py b/containers/backupchecks/src/backend/app/main/routes_inbox.py index b138ff9..f62c863 100644 --- a/containers/backupchecks/src/backend/app/main/routes_inbox.py +++ b/containers/backupchecks/src/backend/app/main/routes_inbox.py @@ -184,6 +184,54 @@ 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() ] + # Optional run_id: if provided and the run is a Cloud Connect run, return + # per-run objects (from run_object_links) and a structured CC summary instead + # of the raw MailObject list which contains all tenants from the shared report email. + cloud_connect_summary = None + run_id_param = request.args.get("run_id", type=int) + if run_id_param: + try: + from ..models import JobRun, CloudConnectAccount + from ..database import db + from sqlalchemy import text as _sql_text + _run = JobRun.query.get(run_id_param) + if _run and getattr(_run, "source_type", None) == "cloud_connect" and _run.job_id: + _cc_acc = CloudConnectAccount.query.filter_by(job_id=_run.job_id).first() + if _cc_acc: + cloud_connect_summary = { + "user": _cc_acc.user or "", + "section": _cc_acc.section or "", + "repo_name": _cc_acc.repo_name or "", + "repo_type": _cc_acc.repo_type or "", + "used_space": _cc_acc.used_space or "", + "total_quota": _cc_acc.total_quota or "", + "free_space": _cc_acc.free_space or "", + "last_active": _cc_acc.last_active_raw or "", + "status": _cc_acc.last_status or "", + } + # Replace MailObject list with per-run objects from run_object_links + cc_rows = db.session.execute( + _sql_text(""" + SELECT co.object_name AS name, rol.status, rol.error_message + FROM run_object_links rol + JOIN customer_objects co ON co.id = rol.customer_object_id + WHERE rol.run_id = :run_id + ORDER BY co.object_name ASC + """), + {"run_id": run_id_param}, + ).mappings().all() + objects = [ + { + "name": r["name"] or "", + "type": "", + "status": r["status"] or "", + "error_message": r["error_message"] or "", + } + for r in cc_rows + ] + except Exception: + pass # keep MailObject objects as fallback + # 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] = [] @@ -227,6 +275,7 @@ def inbox_message_detail(message_id: int): "meta": meta, "body_html": body_html, "objects": objects, + "cloud_connect_summary": cloud_connect_summary, "vspc_companies": vspc_companies, "vspc_company_defaults": vspc_company_defaults, }) diff --git a/containers/backupchecks/src/templates/main/job_detail.html b/containers/backupchecks/src/templates/main/job_detail.html index 098cfa7..b5b0982 100644 --- a/containers/backupchecks/src/templates/main/job_detail.html +++ b/containers/backupchecks/src/templates/main/job_detail.html @@ -258,8 +258,36 @@
Details
-
- + + + + +
+
+
Mail
+ +
+
+ +
@@ -665,7 +693,9 @@ function renderObjects(objects) { if (!messageId) return; currentRunId = runId ? parseInt(runId, 10) : null; - fetch("{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", messageId)) + var detailUrl = "{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", messageId); + if (runId) detailUrl += "?run_id=" + encodeURIComponent(runId); + fetch(detailUrl) .then(function (resp) { if (!resp.ok) throw new Error("Failed to load message details"); return resp.json(); @@ -727,8 +757,33 @@ function renderObjects(objects) { } } - var bodyFrame = document.getElementById("run_msg_body_container_iframe"); - if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || ""); + var ccPanel = document.getElementById("jdm_cc_summary_panel"); + var mailHeading = document.getElementById("jdm_mail_heading"); + var mailToggle = document.getElementById("jdm_mail_toggle"); + var mailBody = document.getElementById("jdm_mail_iframe_body"); + var bodyFrame = document.getElementById("run_msg_body_container_iframe"); + + if (data.cloud_connect_summary) { + var s = data.cloud_connect_summary; + document.getElementById("jdm_cc_user").textContent = s.user || ""; + document.getElementById("jdm_cc_section").textContent = s.section || ""; + document.getElementById("jdm_cc_repo").textContent = s.repo_name + (s.repo_type ? " (" + s.repo_type + ")" : ""); + document.getElementById("jdm_cc_used").textContent = (s.used_space || "—") + " / " + (s.total_quota || "—"); + document.getElementById("jdm_cc_free").textContent = s.free_space || "—"; + document.getElementById("jdm_cc_last_active").textContent = s.last_active || "—"; + document.getElementById("jdm_cc_status").textContent = s.status || "—"; + if (ccPanel) ccPanel.style.display = ""; + if (mailHeading) mailHeading.textContent = "Source report email"; + if (mailToggle) { mailToggle.style.display = ""; mailToggle.textContent = "show"; } + if (mailBody) mailBody.style.display = "none"; + if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || ""); + } else { + if (ccPanel) ccPanel.style.display = "none"; + if (mailHeading) mailHeading.textContent = "Mail"; + if (mailToggle) mailToggle.style.display = "none"; + if (mailBody) mailBody.style.display = ""; + if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || ""); + } renderObjects(data.objects || []); diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 379ef08..2b4a93b 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -5,6 +5,10 @@ This file documents all changes made to this project via Claude Code. ## [2026-03-20] ### Fixed +- Cloud Connect runs in job detail page popup now show a structured CC summary instead of the raw report email with all tenants: + - `routes_inbox.py` (`inbox_message_detail`): accepts optional `?run_id=` parameter; when the run has `source_type = "cloud_connect"`, returns `cloud_connect_summary` dict and per-run objects from `run_object_links` instead of MailObjects + - `job_detail.html`: passes `run_id` to the detail API; if `cloud_connect_summary` is returned, shows the CC summary panel, collapses the raw email (accessible via "show" toggle), and shows only the single per-run repository object + - "Delete all jobs" in Settings → Maintenance no longer times out on large datasets: - Replaced ORM-based deletion (loaded all jobs/runs into Python memory, deleted object by object) with direct SQL `DELETE FROM` statements in FK order — handles 650K+ rows in seconds - Added `job_run_review_events` to the FK cleanup sequence (was causing a FK violation)