diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index b517e89..3c4fa73 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -2,6 +2,7 @@ from .routes_shared import * # noqa: F401,F403 from .routes_shared import _get_database_size_bytes, _get_or_create_settings, _format_bytes, _get_free_disk_bytes, _log_admin_event import json from datetime import datetime +from ..models import CoveAccount, CloudConnectAccount @main_bp.route("/settings/jobs/delete-all", methods=["POST"]) @login_required @@ -451,7 +452,7 @@ def settings_jobs_export(): try: jobs = Job.query.all() payload = { - "schema": "approved_jobs_export_v1", + "schema": "approved_jobs_export_v2", "exported_at": datetime.utcnow().isoformat() + "Z", "counts": {"customers": 0, "jobs": 0}, "customers": [], @@ -475,6 +476,25 @@ def settings_jobs_export(): for job in jobs: customer = customer_by_id.get(job.customer_id) + + cove_acc = getattr(job, "cove_account", None) + cove_data = None + if cove_acc: + cove_data = { + "account_id": cove_acc.account_id, + "account_name": cove_acc.account_name or "", + "computer_name": cove_acc.computer_name or "", + } + + cc_acc = getattr(job, "cloud_connect_account", None) + cc_data = None + if cc_acc: + cc_data = { + "user": cc_acc.user or "", + "section": cc_acc.section or "", + "repo_name": cc_acc.repo_name or "", + } + payload["jobs"].append( { "customer_name": customer.name if customer else None, @@ -488,6 +508,8 @@ def settings_jobs_export(): "schedule_times": job.schedule_times, "auto_approve": bool(job.auto_approve), "active": bool(job.active), + "cove_account": cove_data, + "cloud_connect_account": cc_data, } ) @@ -500,7 +522,7 @@ def settings_jobs_export(): f"Exported jobs configuration", details=json.dumps({ "format": "JSON", - "schema": "approved_jobs_export_v1", + "schema": "approved_jobs_export_v2", "customers_count": len(payload["customers"]), "jobs_count": len(payload["jobs"]) }, indent=2) @@ -537,7 +559,7 @@ def settings_jobs_import(): flash("Invalid JSON file.", "danger") return redirect(url_for("main.settings", section="general")) - if not isinstance(payload, dict) or payload.get("schema") != "approved_jobs_export_v1": + if not isinstance(payload, dict) or payload.get("schema") not in ("approved_jobs_export_v1", "approved_jobs_export_v2"): flash("Unsupported import file schema.", "danger") return redirect(url_for("main.settings", section="general")) @@ -669,6 +691,7 @@ def settings_jobs_import(): existing.auto_approve = auto_approve existing.active = active updated_jobs += 1 + job_record = existing else: job_kwargs = { "customer_id": (customer.id if customer else None), @@ -687,7 +710,41 @@ def settings_jobs_import(): job_kwargs["from_address"] = from_address new_job = Job(**job_kwargs) db.session.add(new_job) + db.session.flush() created_jobs += 1 + job_record = new_job + + # Link Cove account if present in export and not already linked to another job + cove_data = item.get("cove_account") + if cove_data and isinstance(cove_data, dict): + try: + cove_acc = None + if cove_data.get("account_id") is not None: + cove_acc = CoveAccount.query.filter_by(account_id=int(cove_data["account_id"])).first() + if cove_acc is None and cove_data.get("account_name") and cove_data.get("computer_name"): + cove_acc = CoveAccount.query.filter_by( + account_name=cove_data["account_name"], + computer_name=cove_data["computer_name"], + ).first() + if cove_acc and (cove_acc.job_id is None or cove_acc.job_id == job_record.id): + cove_acc.job_id = job_record.id + job_record.cove_account_id = cove_acc.account_id + except Exception: + pass + + # Link Cloud Connect account if present in export and not already linked to another job + cc_data = item.get("cloud_connect_account") + if cc_data and isinstance(cc_data, dict): + try: + cc_acc = CloudConnectAccount.query.filter_by( + user=cc_data.get("user", ""), + section=cc_data.get("section", ""), + repo_name=cc_data.get("repo_name", ""), + ).first() + if cc_acc and (cc_acc.job_id is None or cc_acc.job_id == job_record.id): + cc_acc.job_id = job_record.id + except Exception: + pass db.session.commit() flash( diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 5c8de9e..edfa088 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -2,6 +2,13 @@ This file documents all changes made to this project via Claude Code. +## [2026-03-20] (8) + +### Added +- Jobs export/import now includes Cove and Cloud Connect account links (schema bumped to `approved_jobs_export_v2`): + - **Export**: each job entry now contains a `cove_account` object (`account_id`, `account_name`, `computer_name`) and a `cloud_connect_account` object (`user`, `section`, `repo_name`) when linked; `null` when not linked + - **Import**: accepts both `v1` (no linking) and `v2` files; for `v2` files, after creating/updating the job the importer looks up the matching `CoveAccount` by `account_id` (fallback: `account_name` + `computer_name`) and the matching `CloudConnectAccount` by `user` + `section` + `repo_name`, and links them to the job — only if the account is not yet linked to a different job + ## [2026-03-20] (7) ### Fixed