Merge pull request 'Auto-commit local changes before build (2026-01-08 14:46:19)' (#68) from v20260108-33-runchecks-success-override into main
Reviewed-on: #68
This commit is contained in:
commit
58f0e27dd9
@ -1 +1 @@
|
||||
v20260108-32-runchecks-ticket-copy-button
|
||||
v20260108-33-runchecks-success-override
|
||||
|
||||
@ -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."})
|
||||
|
||||
@ -263,6 +263,7 @@
|
||||
<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_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-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user