| 36352 |
amit |
1 |
<!DOCTYPE html>
|
|
|
2 |
<html lang="en">
|
|
|
3 |
<head>
|
|
|
4 |
<meta charset="UTF-8"/>
|
|
|
5 |
<title>Cron Batches</title>
|
|
|
6 |
<style>
|
|
|
7 |
* { box-sizing: border-box; }
|
|
|
8 |
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 0; background: #f5f6f8; color: #222; }
|
|
|
9 |
header { background: #fff; padding: 16px 24px; border-bottom: 1px solid #e3e5e8; display: flex; align-items: center; justify-content: space-between; }
|
|
|
10 |
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
|
11 |
header .actions { display: flex; gap: 8px; align-items: center; }
|
|
|
12 |
button.refresh { background: #2563eb; color: #fff; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
|
|
13 |
button.refresh:hover { background: #1d4ed8; }
|
|
|
14 |
button.refresh:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
|
15 |
.meta { color: #6b7280; font-size: 13px; }
|
|
|
16 |
main { padding: 16px 24px 48px; }
|
|
|
17 |
.date-group { margin-top: 20px; }
|
|
|
18 |
.date-group h2 { font-size: 13px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; margin: 8px 0; }
|
|
|
19 |
table { width: 100%; background: #fff; border-collapse: collapse; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
|
|
|
20 |
thead th { background: #f9fafb; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
|
|
|
21 |
tbody tr { cursor: pointer; transition: background 0.1s; }
|
|
|
22 |
tbody tr:hover { background: #f3f4f6; }
|
|
|
23 |
tbody td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid #f3f4f6; }
|
|
|
24 |
tbody tr:last-child td { border-bottom: none; }
|
|
|
25 |
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
|
|
|
26 |
.badge-completed { background: #d1fae5; color: #065f46; }
|
|
|
27 |
.badge-partial { background: #fef3c7; color: #92400e; }
|
|
|
28 |
.badge-inprogress { background: #dbeafe; color: #1e40af; }
|
|
|
29 |
.badge-failed { background: #fee2e2; color: #991b1b; }
|
|
|
30 |
.badge-muted { background: #f3f4f6; color: #9ca3af; }
|
|
|
31 |
.counts { font-variant-numeric: tabular-nums; }
|
|
|
32 |
.success-count { color: #059669; font-weight: 600; }
|
|
|
33 |
.failure-count { color: #dc2626; font-weight: 600; }
|
|
|
34 |
.muted { color: #9ca3af; }
|
|
|
35 |
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
|
36 |
.modal-overlay.open { display: flex; }
|
|
|
37 |
.modal { background: #fff; max-width: 900px; width: 95%; max-height: 85vh; display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; }
|
|
|
38 |
.modal-header { padding: 14px 20px; border-bottom: 1px solid #e5e7eb; display: flex; align-items: center; justify-content: space-between; }
|
|
|
39 |
.modal-header h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
|
40 |
.modal-header button { background: none; border: none; font-size: 22px; cursor: pointer; color: #6b7280; line-height: 1; }
|
|
|
41 |
.modal-body { padding: 12px 20px; overflow-y: auto; }
|
|
|
42 |
.row-failed { background: #fef2f2; }
|
|
|
43 |
.row-pending { background: #fffbeb; }
|
|
|
44 |
.empty { text-align: center; color: #9ca3af; padding: 40px 0; }
|
|
|
45 |
.error-text { color: #991b1b; font-family: monospace; font-size: 12px; word-break: break-word; }
|
|
|
46 |
</style>
|
|
|
47 |
</head>
|
|
|
48 |
<body>
|
|
|
49 |
<header>
|
|
|
50 |
<h1>Cron Batches</h1>
|
|
|
51 |
<div class="actions">
|
|
|
52 |
<span class="meta" id="lastUpdated"></span>
|
|
|
53 |
<button class="refresh" id="refreshBtn">Refresh</button>
|
|
|
54 |
</div>
|
|
|
55 |
</header>
|
|
|
56 |
<main>
|
|
|
57 |
<div id="content"></div>
|
|
|
58 |
</main>
|
|
|
59 |
|
|
|
60 |
<div class="modal-overlay" id="modal">
|
|
|
61 |
<div class="modal">
|
|
|
62 |
<div class="modal-header">
|
|
|
63 |
<h3 id="modalTitle">Batch items</h3>
|
|
|
64 |
<button id="modalClose" aria-label="Close">×</button>
|
|
|
65 |
</div>
|
|
|
66 |
<div class="modal-body" id="modalBody"></div>
|
|
|
67 |
</div>
|
|
|
68 |
</div>
|
|
|
69 |
|
|
|
70 |
<script>
|
|
|
71 |
(function () {
|
|
|
72 |
var contentEl = document.getElementById('content');
|
|
|
73 |
var lastUpdatedEl = document.getElementById('lastUpdated');
|
|
|
74 |
var refreshBtn = document.getElementById('refreshBtn');
|
|
|
75 |
var modal = document.getElementById('modal');
|
|
|
76 |
var modalBody = document.getElementById('modalBody');
|
|
|
77 |
var modalTitle = document.getElementById('modalTitle');
|
|
|
78 |
|
|
|
79 |
function el(tag, attrs, text) {
|
|
|
80 |
var e = document.createElement(tag);
|
|
|
81 |
if (attrs) for (var k in attrs) {
|
|
|
82 |
if (k === 'class') e.className = attrs[k];
|
|
|
83 |
else if (k === 'dataset') for (var d in attrs[k]) e.dataset[d] = attrs[k][d];
|
|
|
84 |
else e.setAttribute(k, attrs[k]);
|
|
|
85 |
}
|
|
|
86 |
if (text != null) e.textContent = String(text);
|
|
|
87 |
return e;
|
|
|
88 |
}
|
|
|
89 |
function fmtTime(s) {
|
| 36358 |
amit |
90 |
if (!s) return '-';
|
| 36352 |
amit |
91 |
var t = String(s).replace('T', ' ');
|
|
|
92 |
return t.length > 19 ? t.substring(0, 19) : t;
|
|
|
93 |
}
|
|
|
94 |
function mutedSpan(text) {
|
|
|
95 |
return el('span', {class: 'muted'}, text);
|
|
|
96 |
}
|
|
|
97 |
function timeCell(s) {
|
|
|
98 |
var td = el('td');
|
| 36358 |
amit |
99 |
if (!s) td.appendChild(mutedSpan('-'));
|
| 36352 |
amit |
100 |
else td.textContent = fmtTime(s);
|
|
|
101 |
return td;
|
|
|
102 |
}
|
|
|
103 |
function batchStatusBadge(s) {
|
|
|
104 |
var cls = 'badge badge-muted';
|
| 36358 |
amit |
105 |
var label = s || '-';
|
| 36352 |
amit |
106 |
if (s === 'COMPLETED') cls = 'badge badge-completed';
|
|
|
107 |
else if (s === 'PARTIAL_FAILURE') cls = 'badge badge-partial';
|
|
|
108 |
else if (s === 'IN_PROGRESS') cls = 'badge badge-inprogress';
|
|
|
109 |
else if (s === 'FAILED') cls = 'badge badge-failed';
|
|
|
110 |
return el('span', {class: cls}, label);
|
|
|
111 |
}
|
|
|
112 |
function itemStatusBadge(s) {
|
|
|
113 |
var cls = 'badge badge-muted';
|
| 36358 |
amit |
114 |
var label = s || '-';
|
| 36352 |
amit |
115 |
if (s === 'SUCCESS') cls = 'badge badge-completed';
|
|
|
116 |
else if (s === 'FAILED') cls = 'badge badge-failed';
|
|
|
117 |
else if (s === 'PENDING') cls = 'badge badge-partial';
|
|
|
118 |
return el('span', {class: cls}, label);
|
|
|
119 |
}
|
|
|
120 |
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
|
121 |
|
|
|
122 |
function renderBatches(batches) {
|
|
|
123 |
clear(contentEl);
|
|
|
124 |
if (!batches || batches.length === 0) {
|
|
|
125 |
contentEl.appendChild(el('div', {class: 'empty'}, 'No batches found.'));
|
|
|
126 |
return;
|
|
|
127 |
}
|
|
|
128 |
var groups = {};
|
|
|
129 |
var order = [];
|
|
|
130 |
batches.forEach(function (b) {
|
|
|
131 |
var day = (b.startedAt || '').substring(0, 10) || 'Unknown';
|
|
|
132 |
if (!groups[day]) { groups[day] = []; order.push(day); }
|
|
|
133 |
groups[day].push(b);
|
|
|
134 |
});
|
|
|
135 |
order.forEach(function (day) {
|
|
|
136 |
var group = el('div', {class: 'date-group'});
|
|
|
137 |
group.appendChild(el('h2', null, day));
|
|
|
138 |
var table = el('table');
|
|
|
139 |
var thead = el('thead');
|
|
|
140 |
var headRow = el('tr');
|
|
|
141 |
['ID', 'Job', 'Started', 'Completed', 'Total', 'Success', 'Failed', 'Status'].forEach(function (h) {
|
|
|
142 |
headRow.appendChild(el('th', null, h));
|
|
|
143 |
});
|
|
|
144 |
thead.appendChild(headRow);
|
|
|
145 |
table.appendChild(thead);
|
|
|
146 |
var tbody = el('tbody');
|
|
|
147 |
groups[day].forEach(function (b) {
|
|
|
148 |
var tr = el('tr');
|
|
|
149 |
tr.dataset.batchId = b.id;
|
|
|
150 |
tr.dataset.jobName = b.jobName || '';
|
|
|
151 |
tr.addEventListener('click', onRowClick);
|
|
|
152 |
tr.appendChild(el('td', null, b.id));
|
|
|
153 |
tr.appendChild(el('td', null, b.jobName || ''));
|
|
|
154 |
tr.appendChild(timeCell(b.startedAt));
|
|
|
155 |
tr.appendChild(timeCell(b.completedAt));
|
|
|
156 |
tr.appendChild(el('td', {class: 'counts'}, b.total));
|
|
|
157 |
tr.appendChild(el('td', {class: 'counts success-count'}, b.success));
|
|
|
158 |
tr.appendChild(el('td', {class: 'counts failure-count'}, b.failure || 0));
|
|
|
159 |
var statusTd = el('td');
|
|
|
160 |
statusTd.appendChild(batchStatusBadge(b.status));
|
|
|
161 |
tr.appendChild(statusTd);
|
|
|
162 |
tbody.appendChild(tr);
|
|
|
163 |
});
|
|
|
164 |
table.appendChild(tbody);
|
|
|
165 |
group.appendChild(table);
|
|
|
166 |
contentEl.appendChild(group);
|
|
|
167 |
});
|
|
|
168 |
}
|
|
|
169 |
|
|
|
170 |
function onRowClick(ev) {
|
|
|
171 |
var tr = ev.currentTarget;
|
|
|
172 |
var id = parseInt(tr.dataset.batchId, 10);
|
|
|
173 |
openBatch(id, tr.dataset.jobName || '');
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
function loadBatches() {
|
|
|
177 |
refreshBtn.disabled = true;
|
|
|
178 |
refreshBtn.textContent = 'Refreshing...';
|
|
|
179 |
fetch('/admin/cron-batches/list?limit=200', {credentials: 'same-origin'})
|
|
|
180 |
.then(function (r) { return r.json(); })
|
|
|
181 |
.then(renderBatches)
|
|
|
182 |
.catch(function (e) {
|
|
|
183 |
clear(contentEl);
|
|
|
184 |
contentEl.appendChild(el('div', {class: 'empty'}, 'Failed to load: ' + (e && e.message ? e.message : 'unknown error')));
|
|
|
185 |
})
|
|
|
186 |
.then(function () {
|
|
|
187 |
refreshBtn.disabled = false;
|
|
|
188 |
refreshBtn.textContent = 'Refresh';
|
|
|
189 |
lastUpdatedEl.textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
|
190 |
});
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
function renderItems(items) {
|
|
|
194 |
clear(modalBody);
|
|
|
195 |
if (!items || items.length === 0) {
|
|
|
196 |
modalBody.appendChild(el('div', {class: 'empty'}, 'No items.'));
|
|
|
197 |
return;
|
|
|
198 |
}
|
|
|
199 |
var table = el('table');
|
|
|
200 |
var thead = el('thead');
|
|
|
201 |
var headRow = el('tr');
|
|
|
202 |
['Fofo ID', 'Partner', 'Status', 'Started', 'Completed', 'Error'].forEach(function (h) {
|
|
|
203 |
headRow.appendChild(el('th', null, h));
|
|
|
204 |
});
|
|
|
205 |
thead.appendChild(headRow);
|
|
|
206 |
table.appendChild(thead);
|
|
|
207 |
var tbody = el('tbody');
|
|
|
208 |
items.forEach(function (i) {
|
|
|
209 |
var rowCls = '';
|
|
|
210 |
if (i.status === 'FAILED') rowCls = 'row-failed';
|
|
|
211 |
else if (i.status === 'PENDING') rowCls = 'row-pending';
|
|
|
212 |
var tr = el('tr', {class: rowCls});
|
|
|
213 |
tr.appendChild(el('td', null, i.fofoId));
|
|
|
214 |
tr.appendChild(el('td', null, i.partnerName || ''));
|
|
|
215 |
var statusTd = el('td');
|
|
|
216 |
statusTd.appendChild(itemStatusBadge(i.status));
|
|
|
217 |
tr.appendChild(statusTd);
|
|
|
218 |
tr.appendChild(timeCell(i.startedAt));
|
|
|
219 |
tr.appendChild(timeCell(i.completedAt));
|
|
|
220 |
tr.appendChild(el('td', {class: 'error-text'}, i.errorMessage || ''));
|
|
|
221 |
tbody.appendChild(tr);
|
|
|
222 |
});
|
|
|
223 |
table.appendChild(tbody);
|
|
|
224 |
modalBody.appendChild(table);
|
|
|
225 |
}
|
|
|
226 |
|
|
|
227 |
function openBatch(batchId, jobName) {
|
| 36358 |
amit |
228 |
modalTitle.textContent = 'Batch #' + batchId + (jobName ? ' | ' + jobName : '');
|
| 36352 |
amit |
229 |
clear(modalBody);
|
|
|
230 |
modalBody.appendChild(el('div', {class: 'empty'}, 'Loading items...'));
|
|
|
231 |
modal.classList.add('open');
|
|
|
232 |
fetch('/admin/cron-batches/' + batchId + '/items', {credentials: 'same-origin'})
|
|
|
233 |
.then(function (r) { return r.json(); })
|
|
|
234 |
.then(renderItems)
|
|
|
235 |
.catch(function (e) {
|
|
|
236 |
clear(modalBody);
|
|
|
237 |
modalBody.appendChild(el('div', {class: 'empty'}, 'Failed to load items: ' + (e && e.message ? e.message : 'unknown error')));
|
|
|
238 |
});
|
|
|
239 |
}
|
|
|
240 |
function closeModal() { modal.classList.remove('open'); }
|
|
|
241 |
|
|
|
242 |
refreshBtn.addEventListener('click', loadBatches);
|
|
|
243 |
document.getElementById('modalClose').addEventListener('click', closeModal);
|
|
|
244 |
modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); });
|
|
|
245 |
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });
|
|
|
246 |
|
|
|
247 |
loadBatches();
|
|
|
248 |
})();
|
|
|
249 |
</script>
|
|
|
250 |
</body>
|
|
|
251 |
</html>
|