820 lines
34 KiB
HTML
820 lines
34 KiB
HTML
{% extends "layout/base.html" %}
|
|
|
|
{% block content %}
|
|
<h2 class="mb-3">Daily Jobs</h2>
|
|
|
|
<form method="get" class="row g-3 mb-3">
|
|
<div class="col-auto">
|
|
<label for="dj_date" class="form-label">Date</label>
|
|
<input
|
|
type="date"
|
|
class="form-control"
|
|
id="dj_date"
|
|
name="date"
|
|
value="{{ target_date_str }}"
|
|
/>
|
|
</div>
|
|
<div class="col-auto align-self-end">
|
|
<button type="submit" class="btn btn-primary">Show</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover align-middle" id="dailyJobsTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th scope="col">Customer</th>
|
|
<th scope="col">Backup</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">Job name</th>
|
|
<th scope="col">Time</th>
|
|
<th scope="col">Last result</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if rows %}
|
|
{% for row in rows %}
|
|
<tr
|
|
class="daily-job-row"
|
|
data-job-id="{{ row.job_id }}"
|
|
data-date="{{ target_date_str }}"
|
|
style="cursor: pointer;"
|
|
>
|
|
<td>{{ row.customer_name or "-" }}</td>
|
|
<td>{{ row.backup_software or "-" }}</td>
|
|
<td>{{ row.backup_type or "-" }}</td>
|
|
<td>{{ row.job_name or "-" }}</td>
|
|
<td>{{ row.expected_time }}</td>
|
|
<td>
|
|
{% set _s = (row.last_status or "")|lower %}
|
|
{% set _dot = "" %}
|
|
{% if row.last_override_applied %}{% set _dot = "dot-override" %}
|
|
{% elif _s == "success" %}{% set _dot = "dot-success" %}
|
|
{% elif _s == "warning" %}{% set _dot = "dot-warning" %}
|
|
{% elif _s == "error" %}{% set _dot = "dot-failed" %}
|
|
{% elif _s == "failed" %}{% set _dot = "dot-failed" %}
|
|
{% elif _s == "missed" %}{% set _dot = "dot-missed" %}
|
|
{% elif _s == "expected" %}{% set _dot = "dot-expected" %}
|
|
{% endif %}
|
|
{% if _dot %}<span class="status-dot {{ _dot }} me-2" title="{{ row.last_status }}" aria-label="{{ row.last_status }}" role="img"></span>{% endif %}
|
|
{% if row.has_active_ticket or row.has_active_remark %}
|
|
<span class="ms-2" style="white-space:nowrap;">
|
|
{% if row.has_active_ticket %}<span class="dj-alert-icon dj-alert-ticket" title="Active ticket(s)">🎫</span>{% endif %}
|
|
{% if row.has_active_remark %}<span class="dj-alert-icon dj-alert-remark" title="Active remark(s)">💬</span>{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
{% if row.run_count > 1 %}
|
|
<span class="badge bg-info ms-1">{{ row.run_count }} runs</span>
|
|
{% elif row.run_count == 1 %}
|
|
<span class="badge bg-secondary ms-1">1 run</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-3">
|
|
No jobs are scheduled for this date.
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Modal for daily job runs and mail details -->
|
|
<style>
|
|
/* Daily Jobs popup: match Inbox/Jobs width + keep scrolling INSIDE the popup (not the page). */
|
|
.modal-xxl { max-width: 98vw; }
|
|
@media (min-width: 1400px) { .modal-xxl { max-width: 1400px; } }
|
|
|
|
/* Constrain the modal to the viewport so the page behind it never needs to scroll. */
|
|
#dailyJobModal .modal-content {
|
|
height: 90vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Keep modal-body itself from scrolling; scroll inside the mail/object panels. */
|
|
#dailyJobModal .modal-body {
|
|
overflow: hidden;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
#dailyJobModal #dj_content {
|
|
height: 100%;
|
|
}
|
|
|
|
#dailyJobModal .dj-main-row {
|
|
height: 100%;
|
|
}
|
|
|
|
#dailyJobModal .dj-main-row > .col-md-3 {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
#dailyJobModal .dj-detail-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
#dailyJobModal .dj-mail-panel {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
#dailyJobModal .dj-objects-panel {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
/* Left runs list scrolls inside the popup. */
|
|
#dailyJobModal #dj_runs_list {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
}
|
|
|
|
/* Mail + objects each have their own scroll areas, like Inbox/Jobs. */
|
|
#dailyJobModal #dj_body_iframe { height: 100%; }
|
|
#dailyJobModal .dj-objects-scroll { max-height: 25vh; overflow: auto; }
|
|
</style>
|
|
|
|
<div class="modal fade" id="dailyJobModal" tabindex="-1" aria-labelledby="dailyJobModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable modal-xxl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="dailyJobModalLabel">Job details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="dj_loading" class="text-muted">Loading runs...</div>
|
|
<div id="dj_content" style="display: none;">
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<div class="d-flex flex-wrap gap-3 small text-muted">
|
|
<div><strong>Customer:</strong> <span id="dj_customer"></span></div>
|
|
<div><strong>Backup:</strong> <span id="dj_backup"></span></div>
|
|
<div><strong>Type:</strong> <span id="dj_type"></span></div>
|
|
<div><strong>Job:</strong> <span id="dj_job"></span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 dj-main-row">
|
|
<div class="col-md-3">
|
|
<h6 class="mb-2">Runs</h6>
|
|
<div id="dj_runs_list" class="list-group"></div>
|
|
</div>
|
|
<div class="col-md-9 dj-detail-col">
|
|
<dl class="row mb-3">
|
|
<dt class="col-3">From</dt>
|
|
<dd class="col-9" id="dj_from"></dd>
|
|
|
|
<dt class="col-3">Subject</dt>
|
|
<dd class="col-9" id="dj_subject"></dd>
|
|
|
|
<dt class="col-3">Received</dt>
|
|
<dd class="col-9" id="dj_received"></dd>
|
|
|
|
<dt class="col-3">Status</dt>
|
|
<dd class="col-9" id="dj_status"></dd>
|
|
|
|
<dt class="col-3">Remark</dt>
|
|
<dd class="col-9" id="dj_remark" style="white-space: pre-wrap;"></dd>
|
|
|
|
<dt class="col-3">Meldingen</dt>
|
|
<dd class="col-9">
|
|
<div id="dj_alerts" class="small"></div>
|
|
<div class="mt-2">
|
|
<div class="row g-2 align-items-start">
|
|
<div class="col-12 col-lg-6">
|
|
<div class="border rounded p-2">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="fw-semibold">New ticket</div>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" id="dj_ticket_save">Add</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<textarea class="form-control form-control-sm" id="dj_ticket_description" rows="2" placeholder="Description (optional)"></textarea>
|
|
</div>
|
|
<div class="mt-2 small text-muted" id="dj_ticket_status"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6">
|
|
<div class="border rounded p-2">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="fw-semibold">New remark</div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="dj_remark_save">Add</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<textarea class="form-control form-control-sm" id="dj_remark_body" rows="2" placeholder="Body (required)"></textarea>
|
|
</div>
|
|
<div class="mt-2 small text-muted" id="dj_remark_status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</dd>
|
|
</dl>
|
|
|
|
<div class="mb-3 dj-mail-panel">
|
|
<h6>Mail</h6>
|
|
<iframe
|
|
id="dj_body_iframe"
|
|
class="border rounded"
|
|
style="width:100%;"
|
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
referrerpolicy="no-referrer"
|
|
></iframe>
|
|
</div>
|
|
|
|
<div class="dj-objects-panel">
|
|
<h6>Objects</h6>
|
|
<div class="table-responsive dj-objects-scroll">
|
|
<table class="table table-sm table-bordered" id="dj_objects_table">
|
|
<thead class="table-light" style="position: sticky; top: 0; z-index: 1;">
|
|
<tr>
|
|
<th scope="col">Object</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Error</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="dj_no_runs" class="text-muted" style="display: none;">
|
|
No runs found for this job on the selected date.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a id="dj_eml_btn" class="btn btn-outline-primary" href="#" style="display:none;" rel="nofollow">Download EML</a>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
|
|
function statusClass(status) {
|
|
var s = (status || "").toString().toLowerCase();
|
|
// Allow derived display strings like "Success (override)"
|
|
if (s.indexOf("override") !== -1) return "status-override";
|
|
if (s === "success") return "status-success";
|
|
if (s === "warning") return "status-warning";
|
|
if (s === "error") return "status-error";
|
|
if (s === "failed") return "status-failed";
|
|
if (s === "missed") return "status-missed";
|
|
if (s === "expected") return "status-expected";
|
|
return "";
|
|
}
|
|
|
|
function statusDotClass(status) {
|
|
var s = (status || "").toString().toLowerCase();
|
|
// Allow derived display strings like "Success (override)"
|
|
if (s.indexOf("override") !== -1) return "dot-override";
|
|
if (s === "success") return "dot-success";
|
|
if (s === "warning") return "dot-warning";
|
|
if (s === "error") return "dot-failed";
|
|
if (s === "failed") return "dot-failed";
|
|
if (s === "missed") return "dot-missed";
|
|
if (s === "expected") return "dot-expected";
|
|
return "";
|
|
}
|
|
|
|
|
|
|
|
function wrapMailHtml(html) {
|
|
html = html || "";
|
|
// Ensure we render the mail HTML with its own CSS, isolated from the site styling.
|
|
return (
|
|
"<!doctype html><html><head><meta charset=\"utf-8\">" +
|
|
"<base target=\"_blank\">" +
|
|
"</head><body style=\"margin:0; padding:8px;\">" +
|
|
html +
|
|
"</body></html>"
|
|
);
|
|
}
|
|
|
|
var currentJobId = null;
|
|
var currentRunId = null;
|
|
|
|
function escapeHtml(s) {
|
|
return (s || "").toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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("dj_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>' +
|
|
(t.description ? ('<div class="small text-muted mt-1">' + escapeHtml(t.description) + '</div>') : '') +
|
|
'</div>' +
|
|
'<div class="d-flex gap-1 flex-shrink-0">' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary" data-action="toggle-edit-ticket" data-id="' + t.id + '" ' + (t.resolved_at ? 'disabled' : '') + '>Edit</button>' +
|
|
'<button type="button" class="btn btn-sm btn-outline-success" data-action="resolve-ticket" data-id="' + t.id + '" ' + (t.resolved_at ? 'disabled' : '') + '>Resolve</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="mt-2" data-edit="ticket" style="display:none;">' +
|
|
'<div class="row g-2">' +
|
|
'<div class="col-12">' +
|
|
'<textarea class="form-control form-control-sm" data-field="description" rows="2" placeholder="Description (optional)">' + escapeHtml(t.description || '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="col-12 d-flex gap-2">' +
|
|
'<button type="button" class="btn btn-sm btn-primary" data-action="save-ticket" data-id="' + t.id + '">Save</button>' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary" data-action="cancel-edit" data-id="' + t.id + '">Cancel</button>' +
|
|
'<div class="small text-muted align-self-center" data-field="status"></div>' +
|
|
'</div>' +
|
|
'</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">' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary" data-action="toggle-edit-remark" data-id="' + r.id + '" ' + (r.resolved_at ? 'disabled' : '') + '>Edit</button>' +
|
|
'<button type="button" class="btn btn-sm btn-outline-success" data-action="resolve-remark" data-id="' + r.id + '" ' + (r.resolved_at ? 'disabled' : '') + '>Resolve</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="mt-2" data-edit="remark" style="display:none;">' +
|
|
'<div class="row g-2">' +
|
|
'<div class="col-12">' +
|
|
'<textarea class="form-control form-control-sm" data-field="body" rows="2" placeholder="Body (required)">' + escapeHtml(r.body || '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="col-12 d-flex gap-2">' +
|
|
'<button type="button" class="btn btn-sm btn-primary" data-action="save-remark" data-id="' + r.id + '">Save</button>' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary" data-action="cancel-edit" data-id="' + r.id + '">Cancel</button>' +
|
|
'<div class="small text-muted align-self-center" data-field="status"></div>' +
|
|
'</div>' +
|
|
'</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;
|
|
|
|
var wrapper = btn.closest('[data-alert-type]');
|
|
|
|
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.'); });
|
|
} else if (action === 'toggle-edit-ticket') {
|
|
if (!wrapper) return;
|
|
var edit = wrapper.querySelector('[data-edit="ticket"]');
|
|
if (!edit) return;
|
|
edit.style.display = (edit.style.display === 'none' || !edit.style.display) ? '' : 'none';
|
|
} else if (action === 'toggle-edit-remark') {
|
|
if (!wrapper) return;
|
|
var edit2 = wrapper.querySelector('[data-edit="remark"]');
|
|
if (!edit2) return;
|
|
edit2.style.display = (edit2.style.display === 'none' || !edit2.style.display) ? '' : 'none';
|
|
} else if (action === 'cancel-edit') {
|
|
if (!wrapper) return;
|
|
var editAny = wrapper.querySelector('[data-edit]');
|
|
if (editAny) editAny.style.display = 'none';
|
|
} else if (action === 'save-ticket') {
|
|
if (!wrapper) return;
|
|
var editT = wrapper.querySelector('[data-edit="ticket"]');
|
|
if (!editT) return;
|
|
var descEl = editT.querySelector('[data-field="description"]');
|
|
var statusEl = editT.querySelector('[data-field="status"]');
|
|
var descVal = descEl ? descEl.value : '';
|
|
if (statusEl) statusEl.textContent = 'Saving...';
|
|
apiJson('/api/tickets/' + encodeURIComponent(id), {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({description: descVal})
|
|
})
|
|
.then(function () { loadAlerts(currentRunId); })
|
|
.catch(function (e) {
|
|
if (statusEl) statusEl.textContent = e.message || 'Failed.';
|
|
else alert(e.message || 'Failed.');
|
|
});
|
|
} else if (action === 'save-remark') {
|
|
if (!wrapper) return;
|
|
var editR = wrapper.querySelector('[data-edit="remark"]');
|
|
if (!editR) return;
|
|
var bodyEl2 = editR.querySelector('[data-field="body"]');
|
|
var statusEl2 = editR.querySelector('[data-field="status"]');
|
|
var bodyVal2 = bodyEl2 ? bodyEl2.value : '';
|
|
if (!bodyVal2 || !bodyVal2.trim()) {
|
|
if (statusEl2) statusEl2.textContent = 'Body is required.';
|
|
else alert('Body is required.');
|
|
return;
|
|
}
|
|
if (statusEl2) statusEl2.textContent = 'Saving...';
|
|
apiJson('/api/remarks/' + encodeURIComponent(id), {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({body: bodyVal2})
|
|
})
|
|
.then(function () { loadAlerts(currentRunId); })
|
|
.catch(function (e) {
|
|
if (statusEl2) statusEl2.textContent = e.message || 'Failed.';
|
|
else alert(e.message || 'Failed.');
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadAlerts(runId) {
|
|
var box = document.getElementById('dj_alerts');
|
|
if (!box) return;
|
|
if (!runId) {
|
|
box.innerHTML = '<span class="text-muted">Select a run to manage tickets / remarks.</span>';
|
|
if (window.__djClearCreateStatus) {
|
|
window.__djClearCreateStatus();
|
|
}
|
|
if (window.__djSetCreateDisabled) {
|
|
window.__djSetCreateDisabled(true);
|
|
}
|
|
return;
|
|
}
|
|
box.innerHTML = '<span class="text-muted">Loading alerts...</span>';
|
|
fetch('/api/job-runs/' + encodeURIComponent(runId) + '/alerts')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (j) {
|
|
if (!j || j.status !== 'ok') {
|
|
var msg = (j && j.message) ? j.message : 'Failed to load alerts.';
|
|
throw new Error(msg);
|
|
}
|
|
renderAlerts(j);
|
|
})
|
|
.catch(function (e) {
|
|
box.innerHTML = '<span class="text-danger">' + escapeHtml(e.message || 'Failed') + '</span>';
|
|
});
|
|
}
|
|
|
|
function bindInlineCreateForms() {
|
|
var btnTicket = document.getElementById('dj_ticket_save');
|
|
var btnRemark = document.getElementById('dj_remark_save');
|
|
var tDesc = document.getElementById('dj_ticket_description');
|
|
var tStatus = document.getElementById('dj_ticket_status');
|
|
var rBody = document.getElementById('dj_remark_body');
|
|
var rStatus = document.getElementById('dj_remark_status');
|
|
|
|
function clearStatus() {
|
|
if (tStatus) tStatus.textContent = '';
|
|
if (rStatus) rStatus.textContent = '';
|
|
}
|
|
|
|
function setDisabled(disabled) {
|
|
if (btnTicket) btnTicket.disabled = disabled;
|
|
if (btnRemark) btnRemark.disabled = disabled;
|
|
if (tDesc) tDesc.disabled = disabled;
|
|
if (rBody) rBody.disabled = disabled;
|
|
}
|
|
|
|
window.__djSetCreateDisabled = setDisabled;
|
|
window.__djClearCreateStatus = clearStatus;
|
|
|
|
if (btnTicket) {
|
|
btnTicket.addEventListener('click', function () {
|
|
if (!currentRunId) { alert('Select a run first.'); return; }
|
|
clearStatus();
|
|
var description = tDesc ? tDesc.value : '';
|
|
if (tStatus) tStatus.textContent = 'Saving...';
|
|
apiJson('/api/tickets', {
|
|
method: 'POST',
|
|
body: JSON.stringify({job_run_id: currentRunId, description: description})
|
|
})
|
|
.then(function () {
|
|
if (tDesc) tDesc.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.');
|
|
});
|
|
});
|
|
}
|
|
|
|
setDisabled(true);
|
|
}
|
|
|
|
|
|
function renderRun(data, runIndex) {
|
|
var runs = data.runs || [];
|
|
if (!runs.length) {
|
|
document.getElementById("dj_content").style.display = "none";
|
|
var nr = document.getElementById("dj_no_runs");
|
|
nr.textContent = (data && data.message) ? data.message : "No runs found.";
|
|
nr.style.display = "block";
|
|
return;
|
|
}
|
|
if (runIndex < 0 || runIndex >= runs.length) {
|
|
runIndex = 0; // default to newest run
|
|
}
|
|
var run = runs[runIndex] || {};
|
|
|
|
// Ensure overrides are consistently visible in Daily Jobs.
|
|
var runStatusDisplay = run.status || "";
|
|
if (run.override_applied && runStatusDisplay.toString().toLowerCase().indexOf("override") === -1) {
|
|
runStatusDisplay = (runStatusDisplay ? (runStatusDisplay + " (override)") : "Success (override)");
|
|
}
|
|
|
|
var emlBtn = document.getElementById("dj_eml_btn");
|
|
if (emlBtn) {
|
|
if (run.has_eml && run.mail_message_id) {
|
|
emlBtn.href = "{{ url_for('main.message_eml', message_id=0) }}".replace("0", String(run.mail_message_id));
|
|
emlBtn.style.display = "inline-block";
|
|
} else {
|
|
emlBtn.href = "#";
|
|
emlBtn.style.display = "none";
|
|
}
|
|
}
|
|
|
|
// Highlight active run in the list
|
|
var list = document.getElementById("dj_runs_list");
|
|
if (list) {
|
|
Array.prototype.forEach.call(list.querySelectorAll(".list-group-item"), function (el) {
|
|
if (String(el.getAttribute("data-run-idx")) === String(runIndex)) {
|
|
el.classList.add("active");
|
|
} else {
|
|
el.classList.remove("active");
|
|
}
|
|
});
|
|
}
|
|
|
|
var stEl = document.getElementById("dj_status");
|
|
if (stEl) {
|
|
var d = statusDotClass(runStatusDisplay);
|
|
stEl.innerHTML = (d ? ('<span class="status-dot ' + d + ' me-2" aria-hidden="true"></span>') : '') + escapeHtml(runStatusDisplay || '');
|
|
}
|
|
document.getElementById("dj_remark").textContent = run.remark || "";
|
|
|
|
currentRunId = run.id || null;
|
|
if (window.__djClearCreateStatus) {
|
|
window.__djClearCreateStatus();
|
|
}
|
|
if (window.__djSetCreateDisabled) {
|
|
window.__djSetCreateDisabled(!currentRunId);
|
|
}
|
|
loadAlerts(currentRunId);
|
|
|
|
var mail = run.mail || null;
|
|
if (mail) {
|
|
document.getElementById("dj_from").textContent = mail.from_address || "";
|
|
document.getElementById("dj_subject").textContent = mail.subject || "";
|
|
document.getElementById("dj_received").textContent = mail.received_at || "";
|
|
} else {
|
|
document.getElementById("dj_from").textContent = "";
|
|
document.getElementById("dj_subject").textContent = "";
|
|
document.getElementById("dj_received").textContent = "";
|
|
}
|
|
|
|
var bodyFrame = document.getElementById("dj_body_iframe");
|
|
if (bodyFrame) {
|
|
bodyFrame.srcdoc = wrapMailHtml(run.body_html || "");
|
|
}
|
|
|
|
var tbody = document.querySelector("#dj_objects_table tbody");
|
|
tbody.innerHTML = "";
|
|
(run.objects || []).forEach(function (obj) {
|
|
var tr = document.createElement("tr");
|
|
|
|
var tdName = document.createElement("td");
|
|
tdName.textContent = obj.name || "";
|
|
tr.appendChild(tdName);
|
|
|
|
var tdType = document.createElement("td");
|
|
tdType.textContent = obj.type || "";
|
|
tr.appendChild(tdType);
|
|
|
|
var tdStatus = document.createElement("td");
|
|
tdStatus.className = "status-text " + statusClass(obj.status);
|
|
var d = statusDotClass(obj.status);
|
|
tdStatus.innerHTML = (d ? ('<span class="status-dot ' + d + ' me-2" aria-hidden="true"></span>') : '') + escapeHtml(obj.status || '');
|
|
tr.appendChild(tdStatus);
|
|
|
|
var tdError = document.createElement("td");
|
|
tdError.textContent = obj.error_message || "";
|
|
tr.appendChild(tdError);
|
|
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function attachDailyJobsHandlers() {
|
|
var rows = document.querySelectorAll(".daily-job-row");
|
|
if (!rows.length) {
|
|
return;
|
|
}
|
|
|
|
rows.forEach(function (row) {
|
|
row.addEventListener("click", function () {
|
|
var jobId = row.getAttribute("data-job-id");
|
|
var dateStr = row.getAttribute("data-date");
|
|
if (!jobId || !dateStr) {
|
|
return;
|
|
}
|
|
|
|
currentJobId = jobId;
|
|
|
|
var modalEl = document.getElementById("dailyJobModal");
|
|
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
|
|
document.getElementById("dj_loading").style.display = "block";
|
|
document.getElementById("dj_content").style.display = "none";
|
|
document.getElementById("dj_no_runs").style.display = "none";
|
|
|
|
fetch(
|
|
"{{ url_for('main.daily_jobs_details') }}" +
|
|
"?job_id=" +
|
|
encodeURIComponent(jobId) +
|
|
"&date=" +
|
|
encodeURIComponent(dateStr)
|
|
)
|
|
.then(function (response) {
|
|
var ct = (response.headers && response.headers.get("content-type")) || "";
|
|
if (!response.ok) {
|
|
throw new Error("Failed to load runs (" + response.status + ").");
|
|
}
|
|
if (ct.indexOf("application/json") === -1) {
|
|
throw new Error("Unexpected response while loading runs.");
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(function (data) {
|
|
document.getElementById("dj_loading").style.display = "none";
|
|
|
|
if (!data || data.status !== "ok") {
|
|
var nr = document.getElementById("dj_no_runs");
|
|
nr.textContent = (data && data.message) ? data.message : "No runs found.";
|
|
nr.style.display = "block";
|
|
return;
|
|
}
|
|
|
|
var job = data.job || {};
|
|
var runs = data.runs || [];
|
|
|
|
document.getElementById("dj_customer").textContent = job.customer_name || "";
|
|
document.getElementById("dj_backup").textContent = job.backup_software || "";
|
|
document.getElementById("dj_type").textContent = job.backup_type || "";
|
|
document.getElementById("dj_job").textContent = job.job_name || "";
|
|
document.getElementById("dailyJobModalLabel").textContent =
|
|
(job.job_name || "Job") + " - " + (job.customer_name || "");
|
|
|
|
var listEl = document.getElementById("dj_runs_list");
|
|
listEl.innerHTML = "";
|
|
runs.forEach(function (run, idx) {
|
|
var a = document.createElement("button");
|
|
a.type = "button";
|
|
a.className = "list-group-item list-group-item-action";
|
|
a.setAttribute("data-run-idx", String(idx));
|
|
var rs = run.status || "";
|
|
if (run.override_applied && rs.toString().toLowerCase().indexOf("override") === -1) {
|
|
rs = (rs ? (rs + " (override)") : "Success (override)");
|
|
}
|
|
var label = (run.run_at || "Run") + " - " + (rs || "");
|
|
a.textContent = label;
|
|
a.addEventListener("click", function (ev) {
|
|
ev.preventDefault();
|
|
renderRun(data, idx);
|
|
});
|
|
listEl.appendChild(a);
|
|
});
|
|
|
|
if (runs.length) {
|
|
document.getElementById("dj_content").style.display = "block";
|
|
document.getElementById("dj_no_runs").style.display = "none";
|
|
// runs are returned newest-first
|
|
renderRun(data, 0);
|
|
} else {
|
|
document.getElementById("dj_content").style.display = "none";
|
|
var nr = document.getElementById("dj_no_runs");
|
|
nr.textContent = (data && data.message) ? data.message : "No runs found.";
|
|
nr.style.display = "block";
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
console.error(err);
|
|
try { modal.show(); } catch (e) {}
|
|
document.getElementById("dj_loading").style.display = "none";
|
|
document.getElementById("dj_content").style.display = "none";
|
|
var nr = document.getElementById("dj_no_runs");
|
|
nr.textContent = (err && err.message) ? err.message : "No runs found.";
|
|
nr.style.display = "block";
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
bindInlineCreateForms();
|
|
attachDailyJobsHandlers();
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
{% endblock %}
|