diff --git a/.last-branch b/.last-branch index 43a2923..1a0887d 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260106-24-ticket-scope-resolve-popup +v20260108-25-job-history-ticket-popup diff --git a/containers/backupchecks/src/templates/main/job_detail.html b/containers/backupchecks/src/templates/main/job_detail.html index d97f7c0..749de5b 100644 --- a/containers/backupchecks/src/templates/main/job_detail.html +++ b/containers/backupchecks/src/templates/main/job_detail.html @@ -80,7 +80,7 @@ {% if history_rows %} {% for r in history_rows %} - + {{ r.run_day }} {{ r.run_at }} {% set _s = (r.status or "")|lower %} @@ -184,6 +184,29 @@
Remark
+
Tickets & remarks
+
+
+ + {% if can_manage_jobs %} +
+
New ticket
+
+ + +
+
+ +
New remark
+ +
+ +
+
+
+ {% endif %} +
+
Job
@@ -202,6 +225,7 @@
Parsed
+
@@ -261,7 +285,196 @@ } (function () { - + var currentRunId = null; + + function apiJson(url, opts) { + opts = opts || {}; + opts.headers = opts.headers || {}; + opts.headers['Content-Type'] = 'application/json'; + return fetch(url, opts).then(function (r) { + return r.json().then(function (j) { + if (!r.ok || !j || j.status !== 'ok') { + var msg = (j && j.message) ? j.message : ('Request failed (' + r.status + ')'); + throw new Error(msg); + } + return j; + }); + }); + } + + function renderAlerts(payload) { + var box = document.getElementById('jhm_alerts'); + if (!box) return; + var tickets = (payload && payload.tickets) || []; + var remarks = (payload && payload.remarks) || []; + + if (!tickets.length && !remarks.length) { + box.innerHTML = 'No tickets or remarks linked to this run.'; + return; + } + + var html = ''; + + if (tickets.length) { + html += '
Tickets
'; + tickets.forEach(function (t) { + var status = t.resolved_at ? 'Resolved' : 'Active'; + html += '
' + + '
' + + '
' + + '
' + + '🎫' + + '' + escapeHtml(t.ticket_code || '') + '' + + '' + status + '' + + '
' + + '
' + + '
' + + '{% if can_manage_jobs %}' + + '' + + '{% endif %}' + + '
' + + '
' + + '
'; + }); + html += '
'; + } + + if (remarks.length) { + html += '
Remarks
'; + remarks.forEach(function (r) { + var status = r.resolved_at ? 'Resolved' : 'Active'; + html += '
' + + '
' + + '
' + + '
' + + '💬' + + 'Remark' + + '' + status + '' + + '
' + + (r.body ? ('
' + escapeHtml(r.body) + '
') : '') + + '
' + + '
' + + '{% if can_manage_jobs %}' + + '' + + '{% endif %}' + + '
' + + '
' + + '
'; + }); + html += '
'; + } + + box.innerHTML = html; + + Array.prototype.forEach.call(box.querySelectorAll('button[data-action]'), function (btn) { + btn.addEventListener('click', function (ev) { + ev.preventDefault(); + var action = btn.getAttribute('data-action'); + var id = btn.getAttribute('data-id'); + if (!action || !id) return; + if (action === 'resolve-ticket') { + if (!confirm('Mark ticket as resolved?')) return; + apiJson('/api/tickets/' + encodeURIComponent(id) + '/resolve', {method: 'POST', body: '{}'}) + .then(function () { loadAlerts(currentRunId); }) + .catch(function (e) { alert(e.message || 'Failed.'); }); + } else if (action === 'resolve-remark') { + if (!confirm('Mark remark as resolved?')) return; + apiJson('/api/remarks/' + encodeURIComponent(id) + '/resolve', {method: 'POST', body: '{}'}) + .then(function () { loadAlerts(currentRunId); }) + .catch(function (e) { alert(e.message || 'Failed.'); }); + } + }); + }); + } + + function loadAlerts(runId) { + if (!runId) { + renderAlerts({tickets: [], remarks: []}); + return; + } + fetch('/api/job-runs/' + encodeURIComponent(runId) + '/alerts') + .then(function (r) { return r.json(); }) + .then(function (j) { + if (!j || j.status !== 'ok') throw new Error((j && j.message) || 'Failed'); + renderAlerts(j); + }) + .catch(function () { + renderAlerts({tickets: [], remarks: []}); + }); + } + + function bindInlineCreateForms() { + var btnTicket = document.getElementById('jhm_ticket_save'); + var btnRemark = document.getElementById('jhm_remark_save'); + var tCode = document.getElementById('jhm_ticket_code'); + var tStatus = document.getElementById('jhm_ticket_status'); + var rBody = document.getElementById('jhm_remark_body'); + var rStatus = document.getElementById('jhm_remark_status'); + + function clearStatus() { + if (tStatus) tStatus.textContent = ''; + if (rStatus) rStatus.textContent = ''; + } + + if (btnTicket) { + btnTicket.addEventListener('click', function () { + if (!currentRunId) { alert('Select a run first.'); return; } + clearStatus(); + var ticket_code = tCode ? (tCode.value || '').trim().toUpperCase() : ''; + if (!ticket_code) { + if (tStatus) tStatus.textContent = 'Ticket number is required.'; + else alert('Ticket number is required.'); + return; + } + if (!/^T\d{8}\.\d{4}$/.test(ticket_code)) { + if (tStatus) tStatus.textContent = 'Invalid ticket number format. Expected TYYYYMMDD.####.'; + else alert('Invalid ticket number format. Expected TYYYYMMDD.####.'); + return; + } + if (tStatus) tStatus.textContent = 'Saving...'; + apiJson('/api/tickets', { + method: 'POST', + body: JSON.stringify({job_run_id: currentRunId, ticket_code: ticket_code}) + }) + .then(function () { + if (tCode) tCode.value = ''; + if (tStatus) tStatus.textContent = ''; + loadAlerts(currentRunId); + }) + .catch(function (e) { + if (tStatus) tStatus.textContent = e.message || 'Failed.'; + else alert(e.message || 'Failed.'); + }); + }); + } + + if (btnRemark) { + btnRemark.addEventListener('click', function () { + if (!currentRunId) { alert('Select a run first.'); return; } + clearStatus(); + var body = rBody ? rBody.value : ''; + if (!body || !body.trim()) { + if (rStatus) rStatus.textContent = 'Body is required.'; + else alert('Body is required.'); + return; + } + if (rStatus) rStatus.textContent = 'Saving...'; + apiJson('/api/remarks', { + method: 'POST', + body: JSON.stringify({job_run_id: currentRunId, body: body}) + }) + .then(function () { + if (rBody) rBody.value = ''; + if (rStatus) rStatus.textContent = ''; + loadAlerts(currentRunId); + }) + .catch(function (e) { + if (rStatus) rStatus.textContent = e.message || 'Failed.'; + else alert(e.message || 'Failed.'); + }); + }); + } + } function wrapMailHtml(html) { html = html || ""; @@ -325,10 +538,16 @@ if (!modalEl) return; var modal = new bootstrap.Modal(modalEl); + {% if can_manage_jobs %} + bindInlineCreateForms(); + {% endif %} + rows.forEach(function (row) { row.addEventListener("click", function () { var messageId = row.getAttribute("data-message-id"); + var runId = row.getAttribute("data-run-id"); if (!messageId) return; + currentRunId = runId ? parseInt(runId, 10) : null; fetch("{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", messageId)) .then(function (resp) { @@ -396,6 +615,8 @@ if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || ""); renderObjects(data.objects || []); + + loadAlerts(currentRunId); modal.show(); }) .catch(function (err) { diff --git a/docs/changelog.md b/docs/changelog.md index 66c9e20..cb84dd9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,8 @@ - Prevented duplicate ticket creation errors when reusing an existing ticket_code. - Ensured tickets are reused and linked instead of rejected when already present in the system. +--- + ## v20260106-24-ticket-scope-resolve-popup - Fixed missing ticket number display in job and run popups by always creating or reusing a ticket scope when linking an existing ticket to a job. @@ -11,6 +13,17 @@ - Ensured resolving a ticket from the central Tickets view resolves the ticket globally and closes all associated job scopes. - Updated ticket active status determination to be based on open job scopes, allowing the same ticket number to remain active for other jobs when applicable. +--- + +## v20260108-25-job-history-ticket-popup + +- Added Tickets and Remarks section to the Job History mail popup. +- Aligned ticket handling in Job History with the existing Run Checks popup behavior. +- Enabled viewing of active and resolved tickets and remarks per job run. +- Added support for creating new tickets and remarks from the Job History popup. +- Enabled resolving tickets and remarks directly from the Job History popup. +- Tickets and remarks are now correctly scoped to the selected run (run_id). + ================================================================================================================================================ ## v0.1.17