From af84039daf8e6cedf27ac1f09367a3587c6e2f10 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Fri, 20 Mar 2026 10:22:35 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-03-20 10:22:35) --- .../src/backend/app/cloud_connect_importer.py | 84 +++++++++++++++++++ .../src/backend/app/main/routes_run_checks.py | 7 +- .../src/templates/main/run_checks.html | 66 ++++++++++----- 3 files changed, 131 insertions(+), 26 deletions(-) diff --git a/containers/backupchecks/src/backend/app/cloud_connect_importer.py b/containers/backupchecks/src/backend/app/cloud_connect_importer.py index 415ccda..2da6871 100644 --- a/containers/backupchecks/src/backend/app/cloud_connect_importer.py +++ b/containers/backupchecks/src/backend/app/cloud_connect_importer.py @@ -287,6 +287,10 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict: external_id=external_id, ) db.session.add(run) + db.session.flush() # need run.id for object links + + _persist_cc_objects(row, job.customer_id, job.id, run.id, report_dt) + counters["created"] += 1 counters["linked"] += 1 @@ -294,6 +298,86 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict: return counters +def _persist_cc_objects( + row: dict, + customer_id: int, + job_id: int, + run_id: int, + observed_at: datetime, +) -> None: + """Upsert repository as a customer_object and create a run_object_link. + + This mirrors the Cove datasource object persistence so Cloud Connect runs + have per-run object records suitable for reporting. + """ + from sqlalchemy import text as _text + + repo_name = (row.get("repo_name") or "").strip() or "Unknown Repository" + repo_type = (row.get("repo_type") or "").strip() + object_name = f"{repo_name} ({repo_type})" if repo_type else repo_name + + used = row.get("used_space") or "" + quota = row.get("total_quota") or "" + free = row.get("free_space") or "" + last = row.get("last_active_raw") or "unknown" + + detail = f"Used: {used} / {quota} — Free: {free} — Last active: {last}" + + try: + customer_object_id = db.session.execute( + _text(""" + INSERT INTO customer_objects + (customer_id, object_name, object_type, first_seen_at, last_seen_at) + VALUES + (:customer_id, :object_name, :object_type, NOW(), NOW()) + ON CONFLICT (customer_id, object_name) + DO UPDATE SET + object_type = COALESCE(EXCLUDED.object_type, customer_objects.object_type), + last_seen_at = NOW() + RETURNING id + """), + { + "customer_id": customer_id, + "object_name": object_name, + "object_type": "cloud_connect_repo", + }, + ).scalar() + + db.session.execute( + _text(""" + INSERT INTO job_object_links + (job_id, customer_object_id, first_seen_at, last_seen_at) + VALUES (:job_id, :customer_object_id, NOW(), NOW()) + ON CONFLICT (job_id, customer_object_id) + DO UPDATE SET last_seen_at = NOW() + """), + {"job_id": job_id, "customer_object_id": customer_object_id}, + ) + + db.session.execute( + _text(""" + INSERT INTO run_object_links + (run_id, customer_object_id, status, error_message, observed_at) + VALUES + (:run_id, :customer_object_id, :status, :error_message, :observed_at) + ON CONFLICT (run_id, customer_object_id) + DO UPDATE SET + status = EXCLUDED.status, + error_message = EXCLUDED.error_message, + observed_at = EXCLUDED.observed_at + """), + { + "run_id": run_id, + "customer_object_id": customer_object_id, + "status": row["status"], + "error_message": detail, + "observed_at": observed_at, + }, + ) + except Exception as exc: + logger.warning("CC object persist failed for run %s: %s", run_id, exc) + + def _build_error_message(row: dict) -> str: """Build a human-readable remark for a Cloud Connect run.""" parts = [ diff --git a/containers/backupchecks/src/backend/app/main/routes_run_checks.py b/containers/backupchecks/src/backend/app/main/routes_run_checks.py index e66a07a..b1eb918 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -1494,7 +1494,7 @@ def run_checks_details(): "last_active": _cc_acc.last_active_raw or "", "status": _cc_acc.last_status or "", } - # Keep mail meta and EML link for audit trail; skip body HTML. + # Keep mail meta, EML link, and body for the collapsible source panel. if msg: mail_meta = { "from_address": msg.from_address or "", @@ -1508,7 +1508,7 @@ def run_checks_details(): "subject": msg.subject or "", "received_at": _format_datetime(msg.received_at), } - if msg and cloud_connect_summary is None: + if msg: def _is_blank_text(s): return s is None or (isinstance(s, str) and s.strip() == "") @@ -1584,7 +1584,8 @@ def run_checks_details(): ) # If no run-linked objects exist yet, fall back to objects parsed/stored on the mail message. - if (not objects_payload) and msg: + # Skip this fallback for cloud_connect runs: the mail contains all tenants, not just this one. + if (not objects_payload) and msg and getattr(run, "source_type", None) != "cloud_connect": try: for mo in ( MailObject.query.filter_by(mail_message_id=msg.id) diff --git a/containers/backupchecks/src/templates/main/run_checks.html b/containers/backupchecks/src/templates/main/run_checks.html index 11020b1..1b44d11 100644 --- a/containers/backupchecks/src/templates/main/run_checks.html +++ b/containers/backupchecks/src/templates/main/run_checks.html @@ -318,28 +318,40 @@ -