Subversion Repositories SmartDukaan

Rev

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Cron Batches</title>
    <style>
        * { box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 0; background: #f5f6f8; color: #222; }
        header { background: #fff; padding: 16px 24px; border-bottom: 1px solid #e3e5e8; display: flex; align-items: center; justify-content: space-between; }
        header h1 { margin: 0; font-size: 18px; font-weight: 600; }
        header .actions { display: flex; gap: 8px; align-items: center; }
        button.refresh { background: #2563eb; color: #fff; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
        button.refresh:hover { background: #1d4ed8; }
        button.refresh:disabled { opacity: 0.6; cursor: not-allowed; }
        .meta { color: #6b7280; font-size: 13px; }
        main { padding: 16px 24px 48px; }
        .date-group { margin-top: 20px; }
        .date-group h2 { font-size: 13px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; margin: 8px 0; }
        table { width: 100%; background: #fff; border-collapse: collapse; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
        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; }
        tbody tr { cursor: pointer; transition: background 0.1s; }
        tbody tr:hover { background: #f3f4f6; }
        tbody td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid #f3f4f6; }
        tbody tr:last-child td { border-bottom: none; }
        .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
        .badge-completed { background: #d1fae5; color: #065f46; }
        .badge-partial { background: #fef3c7; color: #92400e; }
        .badge-inprogress { background: #dbeafe; color: #1e40af; }
        .badge-failed { background: #fee2e2; color: #991b1b; }
        .badge-muted { background: #f3f4f6; color: #9ca3af; }
        .counts { font-variant-numeric: tabular-nums; }
        .success-count { color: #059669; font-weight: 600; }
        .failure-count { color: #dc2626; font-weight: 600; }
        .muted { color: #9ca3af; }
        .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; align-items: center; justify-content: center; z-index: 50; }
        .modal-overlay.open { display: flex; }
        .modal { background: #fff; max-width: 900px; width: 95%; max-height: 85vh; display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; }
        .modal-header { padding: 14px 20px; border-bottom: 1px solid #e5e7eb; display: flex; align-items: center; justify-content: space-between; }
        .modal-header h3 { margin: 0; font-size: 16px; font-weight: 600; }
        .modal-header button { background: none; border: none; font-size: 22px; cursor: pointer; color: #6b7280; line-height: 1; }
        .modal-body { padding: 12px 20px; overflow-y: auto; }
        .row-failed { background: #fef2f2; }
        .row-pending { background: #fffbeb; }
        .empty { text-align: center; color: #9ca3af; padding: 40px 0; }
        .error-text { color: #991b1b; font-family: monospace; font-size: 12px; word-break: break-word; }
    </style>
</head>
<body>
<header>
    <h1>Cron Batches</h1>
    <div class="actions">
        <span class="meta" id="lastUpdated"></span>
        <button class="refresh" id="refreshBtn">Refresh</button>
    </div>
</header>
<main>
    <div id="content"></div>
</main>

<div class="modal-overlay" id="modal">
    <div class="modal">
        <div class="modal-header">
            <h3 id="modalTitle">Batch items</h3>
            <button id="modalClose" aria-label="Close">&times;</button>
        </div>
        <div class="modal-body" id="modalBody"></div>
    </div>
</div>

<script>
(function () {
    var contentEl = document.getElementById('content');
    var lastUpdatedEl = document.getElementById('lastUpdated');
    var refreshBtn = document.getElementById('refreshBtn');
    var modal = document.getElementById('modal');
    var modalBody = document.getElementById('modalBody');
    var modalTitle = document.getElementById('modalTitle');

    function el(tag, attrs, text) {
        var e = document.createElement(tag);
        if (attrs) for (var k in attrs) {
            if (k === 'class') e.className = attrs[k];
            else if (k === 'dataset') for (var d in attrs[k]) e.dataset[d] = attrs[k][d];
            else e.setAttribute(k, attrs[k]);
        }
        if (text != null) e.textContent = String(text);
        return e;
    }
    function fmtTime(s) {
        if (!s) return '-';
        var t = String(s).replace('T', ' ');
        return t.length > 19 ? t.substring(0, 19) : t;
    }
    function mutedSpan(text) {
        return el('span', {class: 'muted'}, text);
    }
    function timeCell(s) {
        var td = el('td');
        if (!s) td.appendChild(mutedSpan('-'));
        else td.textContent = fmtTime(s);
        return td;
    }
    function batchStatusBadge(s) {
        var cls = 'badge badge-muted';
        var label = s || '-';
        if (s === 'COMPLETED') cls = 'badge badge-completed';
        else if (s === 'PARTIAL_FAILURE') cls = 'badge badge-partial';
        else if (s === 'IN_PROGRESS') cls = 'badge badge-inprogress';
        else if (s === 'FAILED') cls = 'badge badge-failed';
        return el('span', {class: cls}, label);
    }
    function itemStatusBadge(s) {
        var cls = 'badge badge-muted';
        var label = s || '-';
        if (s === 'SUCCESS') cls = 'badge badge-completed';
        else if (s === 'FAILED') cls = 'badge badge-failed';
        else if (s === 'PENDING') cls = 'badge badge-partial';
        return el('span', {class: cls}, label);
    }
    function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }

    function renderBatches(batches) {
        clear(contentEl);
        if (!batches || batches.length === 0) {
            contentEl.appendChild(el('div', {class: 'empty'}, 'No batches found.'));
            return;
        }
        var groups = {};
        var order = [];
        batches.forEach(function (b) {
            var day = (b.startedAt || '').substring(0, 10) || 'Unknown';
            if (!groups[day]) { groups[day] = []; order.push(day); }
            groups[day].push(b);
        });
        order.forEach(function (day) {
            var group = el('div', {class: 'date-group'});
            group.appendChild(el('h2', null, day));
            var table = el('table');
            var thead = el('thead');
            var headRow = el('tr');
            ['ID', 'Job', 'Started', 'Completed', 'Total', 'Success', 'Failed', 'Status'].forEach(function (h) {
                headRow.appendChild(el('th', null, h));
            });
            thead.appendChild(headRow);
            table.appendChild(thead);
            var tbody = el('tbody');
            groups[day].forEach(function (b) {
                var tr = el('tr');
                tr.dataset.batchId = b.id;
                tr.dataset.jobName = b.jobName || '';
                tr.addEventListener('click', onRowClick);
                tr.appendChild(el('td', null, b.id));
                tr.appendChild(el('td', null, b.jobName || ''));
                tr.appendChild(timeCell(b.startedAt));
                tr.appendChild(timeCell(b.completedAt));
                tr.appendChild(el('td', {class: 'counts'}, b.total));
                tr.appendChild(el('td', {class: 'counts success-count'}, b.success));
                tr.appendChild(el('td', {class: 'counts failure-count'}, b.failure || 0));
                var statusTd = el('td');
                statusTd.appendChild(batchStatusBadge(b.status));
                tr.appendChild(statusTd);
                tbody.appendChild(tr);
            });
            table.appendChild(tbody);
            group.appendChild(table);
            contentEl.appendChild(group);
        });
    }

    function onRowClick(ev) {
        var tr = ev.currentTarget;
        var id = parseInt(tr.dataset.batchId, 10);
        openBatch(id, tr.dataset.jobName || '');
    }

    function loadBatches() {
        refreshBtn.disabled = true;
        refreshBtn.textContent = 'Refreshing...';
        fetch('/admin/cron-batches/list?limit=200', {credentials: 'same-origin'})
            .then(function (r) { return r.json(); })
            .then(renderBatches)
            .catch(function (e) {
                clear(contentEl);
                contentEl.appendChild(el('div', {class: 'empty'}, 'Failed to load: ' + (e && e.message ? e.message : 'unknown error')));
            })
            .then(function () {
                refreshBtn.disabled = false;
                refreshBtn.textContent = 'Refresh';
                lastUpdatedEl.textContent = 'Updated ' + new Date().toLocaleTimeString();
            });
    }

    function renderItems(items) {
        clear(modalBody);
        if (!items || items.length === 0) {
            modalBody.appendChild(el('div', {class: 'empty'}, 'No items.'));
            return;
        }
        var table = el('table');
        var thead = el('thead');
        var headRow = el('tr');
        ['Fofo ID', 'Partner', 'Status', 'Started', 'Completed', 'Error'].forEach(function (h) {
            headRow.appendChild(el('th', null, h));
        });
        thead.appendChild(headRow);
        table.appendChild(thead);
        var tbody = el('tbody');
        items.forEach(function (i) {
            var rowCls = '';
            if (i.status === 'FAILED') rowCls = 'row-failed';
            else if (i.status === 'PENDING') rowCls = 'row-pending';
            var tr = el('tr', {class: rowCls});
            tr.appendChild(el('td', null, i.fofoId));
            tr.appendChild(el('td', null, i.partnerName || ''));
            var statusTd = el('td');
            statusTd.appendChild(itemStatusBadge(i.status));
            tr.appendChild(statusTd);
            tr.appendChild(timeCell(i.startedAt));
            tr.appendChild(timeCell(i.completedAt));
            tr.appendChild(el('td', {class: 'error-text'}, i.errorMessage || ''));
            tbody.appendChild(tr);
        });
        table.appendChild(tbody);
        modalBody.appendChild(table);
    }

    function openBatch(batchId, jobName) {
        modalTitle.textContent = 'Batch #' + batchId + (jobName ? ' | ' + jobName : '');
        clear(modalBody);
        modalBody.appendChild(el('div', {class: 'empty'}, 'Loading items...'));
        modal.classList.add('open');
        fetch('/admin/cron-batches/' + batchId + '/items', {credentials: 'same-origin'})
            .then(function (r) { return r.json(); })
            .then(renderItems)
            .catch(function (e) {
                clear(modalBody);
                modalBody.appendChild(el('div', {class: 'empty'}, 'Failed to load items: ' + (e && e.message ? e.message : 'unknown error')));
            });
    }
    function closeModal() { modal.classList.remove('open'); }

    refreshBtn.addEventListener('click', loadBatches);
    document.getElementById('modalClose').addEventListener('click', closeModal);
    modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); });
    document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });

    loadBatches();
})();
</script>
</body>
</html>