Auto-commit local changes before build (2026-01-08 14:46:19) #68

Merged
ivooskamp merged 1 commits from v20260108-33-runchecks-success-override into main 2026-01-13 11:27:06 +01:00
4 changed files with 191 additions and 1 deletions

View File

@ -1 +1 @@
v20260108-32-runchecks-ticket-copy-button v20260108-33-runchecks-success-override

View File

@ -30,6 +30,7 @@ from ..models import (
JobRunReviewEvent, JobRunReviewEvent,
MailMessage, MailMessage,
MailObject, MailObject,
Override,
User, User,
) )
@ -38,6 +39,15 @@ from ..models import (
MISSED_GRACE_WINDOW = timedelta(hours=1) 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: def _utc_naive_from_local(dt_local: datetime) -> datetime:
"""Convert a timezone-aware local datetime to UTC naive, matching DB convention.""" """Convert a timezone-aware local datetime to UTC naive, matching DB convention."""
if dt_local.tzinfo is None: if dt_local.tzinfo is None:
@ -822,3 +832,146 @@ def api_run_checks_unmark_reviewed():
db.session.commit() db.session.commit()
return jsonify({"status": "ok", "updated": updated, "skipped": skipped}) 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."})

View File

@ -263,6 +263,7 @@
<div class="modal-footer"> <div class="modal-footer">
<a id="rcm_eml_btn" class="btn btn-outline-primary" href="#" style="display:none;" rel="nofollow">Download EML</a> <a id="rcm_eml_btn" class="btn btn-outline-primary" href="#" style="display:none;" rel="nofollow">Download EML</a>
<a id="rcm_job_btn" class="btn btn-outline-secondary" href="#">Open job page</a> <a id="rcm_job_btn" class="btn btn-outline-secondary" href="#">Open job page</a>
<button type="button" class="btn btn-outline-primary" id="rcm_mark_success_override" disabled>Mark success (override)</button>
<button type="button" class="btn btn-primary" id="rcm_mark_all_reviewed" disabled>Mark as Reviewed</button> <button type="button" class="btn btn-primary" id="rcm_mark_all_reviewed" disabled>Mark as Reviewed</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
@ -285,6 +286,7 @@
var currentPayload = null; var currentPayload = null;
var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed'); var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed');
var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override');
// Shift-click range selection for checkbox rows // Shift-click range selection for checkbox rows
var lastCheckedCb = null; 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) { function renderAlerts(payload) {
var box = document.getElementById('rcm_alerts'); var box = document.getElementById('rcm_alerts');
if (!box) return; if (!box) return;
@ -882,6 +903,11 @@ if (tStatus) tStatus.textContent = '';
currentRunId = run.id || null; currentRunId = run.id || null;
if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus(); if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus();
if (window.__rcmSetCreateDisabled) window.__rcmSetCreateDisabled(!currentRunId); 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); loadAlerts(currentRunId);
var mail = run.mail || null; var mail = run.mail || null;
@ -945,6 +971,7 @@ if (tStatus) tStatus.textContent = '';
currentJobId = jobId; currentJobId = jobId;
if (btnMarkAllReviewed) btnMarkAllReviewed.disabled = true; if (btnMarkAllReviewed) btnMarkAllReviewed.disabled = true;
if (btnMarkSuccessOverride) btnMarkSuccessOverride.disabled = true;
var modalEl = document.getElementById('runChecksModal'); var modalEl = document.getElementById('runChecksModal');
var modal = bootstrap.Modal.getOrCreateInstance(modalEl); var modal = bootstrap.Modal.getOrCreateInstance(modalEl);

View File

@ -53,6 +53,16 @@
- Implemented copy-to-clipboard functionality to copy only the ticket number. - 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. - 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 ## v0.1.18