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,
|
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."})
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user