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
-
-
+
+
+
+
Cloud Connect
+
+ - User
+ - Section
+ - Repository
+ - Used / Quota
+ - Free
+ - Last active
+ - Status
+
+
+
+
@@ -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)