Merge pull request 'Auto-commit local changes before build (2026-01-08 10:13:48)' (#59) from v20260108-25-job-history-ticket-popup into main
Reviewed-on: #59
This commit is contained in:
commit
a38fe43613
@ -1 +1 @@
|
|||||||
v20260106-24-ticket-scope-resolve-popup
|
v20260108-25-job-history-ticket-popup
|
||||||
|
|||||||
@ -80,7 +80,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% if history_rows %}
|
{% if history_rows %}
|
||||||
{% for r in history_rows %}
|
{% for r in history_rows %}
|
||||||
<tr{% if r.mail_message_id %} class="jobrun-row" data-message-id="{{ r.mail_message_id }}" data-ticket-codes="{{ (r.ticket_codes or [])|tojson|forceescape }}" data-remark-items="{{ (r.remark_items or [])|tojson|forceescape }}" style="cursor: pointer;"{% endif %}>
|
<tr{% if r.mail_message_id %} class="jobrun-row" data-message-id="{{ r.mail_message_id }}" data-run-id="{{ r.id }}" data-ticket-codes="{{ (r.ticket_codes or [])|tojson|forceescape }}" data-remark-items="{{ (r.remark_items or [])|tojson|forceescape }}" style="cursor: pointer;"{% endif %}>
|
||||||
<td>{{ r.run_day }}</td>
|
<td>{{ r.run_day }}</td>
|
||||||
<td>{{ r.run_at }}</td>
|
<td>{{ r.run_at }}</td>
|
||||||
{% set _s = (r.status or "")|lower %}
|
{% set _s = (r.status or "")|lower %}
|
||||||
@ -184,6 +184,29 @@
|
|||||||
<dt class="col-4">Remark</dt>
|
<dt class="col-4">Remark</dt>
|
||||||
<dd class="col-8" id="run_msg_remark"></dd>
|
<dd class="col-8" id="run_msg_remark"></dd>
|
||||||
|
|
||||||
|
<dt class="col-12 mt-2">Tickets & remarks</dt>
|
||||||
|
<dd class="col-12">
|
||||||
|
<div id="jhm_alerts" class="small"></div>
|
||||||
|
|
||||||
|
{% if can_manage_jobs %}
|
||||||
|
<div class="border rounded p-2 mt-2">
|
||||||
|
<div class="fw-semibold">New ticket</div>
|
||||||
|
<div class="d-flex gap-2 mt-1">
|
||||||
|
<input class="form-control form-control-sm" id="jhm_ticket_code" type="text" placeholder="Ticket number (e.g., T20260106.0001)" />
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="jhm_ticket_save">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 small text-muted" id="jhm_ticket_status"></div>
|
||||||
|
|
||||||
|
<div class="fw-semibold mt-2">New remark</div>
|
||||||
|
<textarea class="form-control form-control-sm" id="jhm_remark_body" rows="2" placeholder="Remark"></textarea>
|
||||||
|
<div class="d-flex justify-content-end mt-1">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="jhm_remark_save">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 small text-muted" id="jhm_remark_status"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-4">Job</dt>
|
<dt class="col-4">Job</dt>
|
||||||
<dd class="col-8" id="run_msg_job"></dd>
|
<dd class="col-8" id="run_msg_job"></dd>
|
||||||
|
|
||||||
@ -202,6 +225,7 @@
|
|||||||
<dt class="col-4">Parsed</dt>
|
<dt class="col-4">Parsed</dt>
|
||||||
<dd class="col-8" id="run_msg_parsed"></dd>
|
<dd class="col-8" id="run_msg_parsed"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@ -261,7 +285,196 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
(function () {
|
(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 = '<span class="text-muted">No tickets or remarks linked to this run.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (tickets.length) {
|
||||||
|
html += '<div class="mb-2"><strong>Tickets</strong><div class="mt-1">';
|
||||||
|
tickets.forEach(function (t) {
|
||||||
|
var status = t.resolved_at ? 'Resolved' : 'Active';
|
||||||
|
html += '<div class="mb-2 border rounded p-2" data-alert-type="ticket" data-id="' + t.id + '">' +
|
||||||
|
'<div class="d-flex align-items-start justify-content-between gap-2">' +
|
||||||
|
'<div class="flex-grow-1 min-w-0">' +
|
||||||
|
'<div class="text-truncate">' +
|
||||||
|
'<span class="me-1" title="Ticket">🎫</span>' +
|
||||||
|
'<span class="fw-semibold">' + escapeHtml(t.ticket_code || '') + '</span>' +
|
||||||
|
'<span class="ms-2 badge ' + (t.resolved_at ? 'bg-secondary' : 'bg-warning text-dark') + '">' + status + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="d-flex gap-1 flex-shrink-0">' +
|
||||||
|
'{% if can_manage_jobs %}' +
|
||||||
|
'<button type="button" class="btn btn-sm btn-outline-success" data-action="resolve-ticket" data-id="' + t.id + '" ' + (t.resolved_at ? 'disabled' : '') + '>Resolve</button>' +
|
||||||
|
'{% endif %}' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remarks.length) {
|
||||||
|
html += '<div class="mb-2"><strong>Remarks</strong><div class="mt-1">';
|
||||||
|
remarks.forEach(function (r) {
|
||||||
|
var status = r.resolved_at ? 'Resolved' : 'Active';
|
||||||
|
html += '<div class="mb-2 border rounded p-2" data-alert-type="remark" data-id="' + r.id + '">' +
|
||||||
|
'<div class="d-flex align-items-start justify-content-between gap-2">' +
|
||||||
|
'<div class="flex-grow-1 min-w-0">' +
|
||||||
|
'<div class="text-truncate">' +
|
||||||
|
'<span class="me-1" title="Remark">💬</span>' +
|
||||||
|
'<span class="fw-semibold">Remark</span>' +
|
||||||
|
'<span class="ms-2 badge ' + (r.resolved_at ? 'bg-secondary' : 'bg-warning text-dark') + '">' + status + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
(r.body ? ('<div class="small text-muted mt-1">' + escapeHtml(r.body) + '</div>') : '') +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="d-flex gap-1 flex-shrink-0">' +
|
||||||
|
'{% if can_manage_jobs %}' +
|
||||||
|
'<button type="button" class="btn btn-sm btn-outline-success" data-action="resolve-remark" data-id="' + r.id + '" ' + (r.resolved_at ? 'disabled' : '') + '>Resolve</button>' +
|
||||||
|
'{% endif %}' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
function wrapMailHtml(html) {
|
||||||
html = html || "";
|
html = html || "";
|
||||||
@ -325,10 +538,16 @@
|
|||||||
if (!modalEl) return;
|
if (!modalEl) return;
|
||||||
var modal = new bootstrap.Modal(modalEl);
|
var modal = new bootstrap.Modal(modalEl);
|
||||||
|
|
||||||
|
{% if can_manage_jobs %}
|
||||||
|
bindInlineCreateForms();
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
rows.forEach(function (row) {
|
rows.forEach(function (row) {
|
||||||
row.addEventListener("click", function () {
|
row.addEventListener("click", function () {
|
||||||
var messageId = row.getAttribute("data-message-id");
|
var messageId = row.getAttribute("data-message-id");
|
||||||
|
var runId = row.getAttribute("data-run-id");
|
||||||
if (!messageId) return;
|
if (!messageId) return;
|
||||||
|
currentRunId = runId ? parseInt(runId, 10) : null;
|
||||||
|
|
||||||
fetch("{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", messageId))
|
fetch("{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", messageId))
|
||||||
.then(function (resp) {
|
.then(function (resp) {
|
||||||
@ -396,6 +615,8 @@
|
|||||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
||||||
|
|
||||||
renderObjects(data.objects || []);
|
renderObjects(data.objects || []);
|
||||||
|
|
||||||
|
loadAlerts(currentRunId);
|
||||||
modal.show();
|
modal.show();
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
- Prevented duplicate ticket creation errors when reusing an existing ticket_code.
|
- 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.
|
- Ensured tickets are reused and linked instead of rejected when already present in the system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v20260106-24-ticket-scope-resolve-popup
|
## 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.
|
- 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.
|
- 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.
|
- 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
|
## v0.1.17
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user