From 87581f825fd460d9a367acad84d8ba1f32651dc8 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 8 Jan 2026 14:46:19 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-08 14:46:19) --- .last-branch | 2 +- .../src/backend/app/main/routes_run_checks.py | 153 ++++++++++++++++++ .../src/templates/main/run_checks.html | 27 ++++ docs/changelog.md | 10 ++ 4 files changed, 191 insertions(+), 1 deletion(-) diff --git a/.last-branch b/.last-branch index bad35ef..88e898b 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260108-32-runchecks-ticket-copy-button +v20260108-33-runchecks-success-override 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 2ecfb7f..0263157 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -30,6 +30,7 @@ from ..models import ( JobRunReviewEvent, MailMessage, MailObject, + Override, User, ) @@ -38,6 +39,15 @@ from ..models import ( MISSED_GRACE_WINDOW = timedelta(hours=1) +def _status_is_success(status: str | None) -> bool: + s = (status or "").strip().lower() + if not s: + return False + if "override" in s: + return True + return "success" in s + + def _utc_naive_from_local(dt_local: datetime) -> datetime: """Convert a timezone-aware local datetime to UTC naive, matching DB convention.""" if dt_local.tzinfo is None: @@ -822,3 +832,146 @@ def api_run_checks_unmark_reviewed(): db.session.commit() return jsonify({"status": "ok", "updated": updated, "skipped": skipped}) + + +@main_bp.post("/api/run-checks/mark-success-override") +@login_required +@roles_required("admin", "operator") +def api_run_checks_mark_success_override(): + """Create a time-bounded override so the selected run is treated as Success (override).""" + data = request.get_json(silent=True) or {} + try: + run_id = int(data.get("run_id") or 0) + except Exception: + run_id = 0 + + if run_id <= 0: + return jsonify({"status": "error", "message": "Invalid run_id."}), 400 + + run = JobRun.query.get_or_404(run_id) + job = Job.query.get_or_404(run.job_id) + + # Do not allow overriding a missed placeholder run. + if bool(getattr(run, "missed", False)): + return jsonify({"status": "error", "message": "Missed runs cannot be marked as success."}), 400 + + # If it is already a success or already overridden, do nothing. + if bool(getattr(run, "override_applied", False)): + return jsonify({"status": "ok", "message": "Already overridden."}) + + if _status_is_success(getattr(run, "status", None)): + return jsonify({"status": "ok", "message": "Already successful."}) + + # Build a tight validity window around this run. + run_ts = getattr(run, "run_at", None) or getattr(run, "created_at", None) or datetime.utcnow() + start_at = run_ts - timedelta(minutes=1) + end_at = run_ts + timedelta(minutes=1) + + comment = (data.get("comment") or "").strip() + if not comment: + # Keep it short and consistent; Operators will typically include a ticket number separately. + comment = "Marked as success from Run Checks" + comment = comment[:2000] + + created_any = False + + # Prefer object-level overrides (scoped to this job) to avoid impacting other jobs. + obj_rows = [] + try: + obj_rows = ( + db.session.execute( + text( + """ + SELECT + co.object_name AS object_name, + rol.status AS status, + rol.error_message AS 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}, + ) + .mappings() + .all() + ) + except Exception: + obj_rows = [] + + def _obj_is_problem(status: str | None) -> bool: + s = (status or "").strip().lower() + if not s: + return False + if "success" in s: + return False + if "override" in s: + return False + return True + + for rr in obj_rows or []: + obj_name = (rr.get("object_name") or "").strip() + obj_status = (rr.get("status") or "").strip() + if (not obj_name) or (not _obj_is_problem(obj_status)): + continue + + err = (rr.get("error_message") or "").strip() + ov = Override( + level="object", + job_id=job.id, + object_name=obj_name, + match_status=(obj_status or None), + match_error_contains=(err[:255] if err else None), + treat_as_success=True, + active=True, + comment=comment, + created_by=current_user.username, + start_at=start_at, + end_at=end_at, + ) + db.session.add(ov) + created_any = True + + # If we couldn't build a safe object-scoped override, fall back to a very tight global override. + if not created_any: + match_error_contains = (getattr(run, "remark", None) or "").strip() + if not match_error_contains: + # As a last resort, try to match any error message from legacy objects. + try: + objs = list(run.objects) if hasattr(run, "objects") else [] + except Exception: + objs = [] + for obj in objs or []: + em = (getattr(obj, "error_message", None) or "").strip() + if em: + match_error_contains = em + break + + ov = Override( + level="global", + backup_software=job.backup_software or None, + backup_type=job.backup_type or None, + match_status=(getattr(run, "status", None) or None), + match_error_contains=(match_error_contains[:255] if match_error_contains else None), + treat_as_success=True, + active=True, + comment=comment, + created_by=current_user.username, + start_at=start_at, + end_at=end_at, + ) + db.session.add(ov) + created_any = True + + db.session.commit() + + # Recompute flags so the overview and modal reflect the override immediately. + try: + from .routes_shared import _recompute_override_flags_for_runs + + _recompute_override_flags_for_runs(job_ids=[job.id], start_at=start_at, end_at=end_at, only_unreviewed=False) + except Exception: + pass + + return jsonify({"status": "ok", "message": "Override created."}) diff --git a/containers/backupchecks/src/templates/main/run_checks.html b/containers/backupchecks/src/templates/main/run_checks.html index 58b2d11..597f464 100644 --- a/containers/backupchecks/src/templates/main/run_checks.html +++ b/containers/backupchecks/src/templates/main/run_checks.html @@ -263,6 +263,7 @@ @@ -285,6 +286,7 @@ var currentPayload = null; var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed'); + var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override'); // Shift-click range selection for checkbox rows var lastCheckedCb = null; @@ -645,6 +647,25 @@ table.addEventListener('change', function (e) { }); } + if (btnMarkSuccessOverride) { + btnMarkSuccessOverride.addEventListener('click', function () { + if (!currentRunId) return; + btnMarkSuccessOverride.disabled = true; + apiJson('/api/run-checks/mark-success-override', { + method: 'POST', + body: JSON.stringify({ run_id: currentRunId }) + }) + .then(function (j) { + if (!j || j.status !== 'ok') throw new Error((j && j.message) || 'Failed'); + window.location.reload(); + }) + .catch(function (e) { + alert((e && e.message) ? e.message : 'Failed to mark as success (override).'); + btnMarkSuccessOverride.disabled = false; + }); + }); + } + function renderAlerts(payload) { var box = document.getElementById('rcm_alerts'); if (!box) return; @@ -882,6 +903,11 @@ if (tStatus) tStatus.textContent = ''; currentRunId = run.id || null; if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus(); if (window.__rcmSetCreateDisabled) window.__rcmSetCreateDisabled(!currentRunId); + if (btnMarkSuccessOverride) { + var _rs = (run.status || '').toString().toLowerCase(); + var _canOverride = !!currentRunId && !run.missed && (_rs.indexOf('override') === -1) && (_rs.indexOf('success') === -1); + btnMarkSuccessOverride.disabled = !_canOverride; + } loadAlerts(currentRunId); var mail = run.mail || null; @@ -945,6 +971,7 @@ if (tStatus) tStatus.textContent = ''; currentJobId = jobId; if (btnMarkAllReviewed) btnMarkAllReviewed.disabled = true; + if (btnMarkSuccessOverride) btnMarkSuccessOverride.disabled = true; var modalEl = document.getElementById('runChecksModal'); var modal = bootstrap.Modal.getOrCreateInstance(modalEl); diff --git a/docs/changelog.md b/docs/changelog.md index 1149530..9dbc301 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -53,6 +53,16 @@ - Implemented copy-to-clipboard functionality to copy only the ticket number. - Prevented accidental selection of the appended status text (e.g. “Active”) when copying tickets. +--- + +## v20260108-33-runchecks-success-override + +- Added a manual “Success (override)” action in the Run Checks popup. +- Operators and Admins can mark a backup run as successful even if it originally failed or warned. +- Implemented backend support to store the override state. +- Updated UI logic so overridden runs are displayed with the blue success indicator. +- Ensured the override only affects the selected run and does not modify original run data. + ================================================================================================================================================ ## v0.1.18