Subversion Repositories SmartDukaan

Rev

Rev 36352 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
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">&times;</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>