{"id":1056,"date":"2025-08-25T01:36:21","date_gmt":"2025-08-25T01:36:21","guid":{"rendered":"https:\/\/abatablaster.xyz\/?page_id=1056"},"modified":"2026-03-10T03:51:47","modified_gmt":"2026-03-10T03:51:47","slug":"smart-grabber","status":"publish","type":"page","link":"https:\/\/abatablaster.xyz\/index.php\/smart-grabber\/","title":{"rendered":"Smart Grabber"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"ms\">\n\n<head>\n    <meta charset=\"UTF-8\" \/>\n    <title>Smart Grabber (Video \u2192 CSV) \u2013 AbataBlaster<\/title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n    <!-- Firebase compat SDKs (sama pattern dengan fail anda yang lain) -->\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-app-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-auth-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-firestore-compat.js\"><\/script>\n    <!-- SweetAlert -->\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/sweetalert2@11\"><\/script>\n\n    <style>\n        :root {\n            --brand: #22c55e;\n            --brand-700: #16a34a;\n            --ink: #1b5e20;\n            --bg: #e8f5e9;\n            --card: #fff;\n            --muted: #6b7280;\n        }\n\n        * {\n            box-sizing: border-box\n        }\n\n        body {\n            margin: 0;\n            background: var(--bg);\n            font-family: system-ui, Segoe UI, Roboto, Arial, sans-serif;\n            color: #0f172a\n        }\n\n        .wrap {\n            max-width: 860px;\n            margin: 28px auto;\n            padding: 0 16px\n        }\n\n        .logo {\n            display: block;\n            margin: 6px auto 14px auto;\n            max-width: 180px;\n            border-radius: 12px;\n            box-shadow: 0 4px 10px rgba(0, 0, 0, .15)\n        }\n\n        .card {\n            background: var(--card);\n            border-radius: 16px;\n            box-shadow: 0 6px 22px rgba(0, 0, 0, .08);\n            padding: 18px\n        }\n\n        h1 {\n            margin: 4px 0 12px 0;\n            font-size: 1.4rem;\n            color: var(--ink)\n        }\n\n        .grid {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 12px\n        }\n\n        @media (max-width:640px) {\n            .grid {\n                grid-template-columns: 1fr\n            }\n        }\n\n        label {\n            font-weight: 600;\n            font-size: .95rem\n        }\n\n        input[type=\"number\"],\n        input[type=\"text\"],\n        select {\n            width: 100%;\n            padding: 10px;\n            border: 1px solid #c8e6c9;\n            border-radius: 10px\n        }\n\n        .dropzone {\n            border: 2px dashed #94d3a2;\n            border-radius: 14px;\n            background: #f7fbf7;\n            padding: 18px;\n            text-align: center;\n            color: #115e2f;\n            cursor: pointer;\n            transition: .15s;\n        }\n\n        .dropzone:hover {\n            background: #eef9f1\n        }\n\n        .muted {\n            color: var(--muted);\n            font-size: .9rem\n        }\n\n        .btn {\n            appearance: none;\n            border: 0;\n            border-radius: 10px;\n            background: var(--brand);\n            color: #fff;\n            padding: 12px 16px;\n            font-weight: 700;\n            cursor: pointer\n        }\n\n        .btn:hover {\n            background: var(--brand-700)\n        }\n\n        .btn-outline {\n            background: #fff;\n            color: var(--brand);\n            border: 1px solid #94d3a2\n        }\n\n        .bar {\n            height: 10px;\n            background: #e5f5ea;\n            border-radius: 999px;\n            overflow: hidden\n        }\n\n        .bar>div {\n            height: 100%;\n            width: 0;\n            background: linear-gradient(90deg, #22c55e, #86efac)\n        }\n\n        .row {\n            display: flex;\n            gap: 10px;\n            align-items: center;\n            flex-wrap: wrap\n        }\n\n        .mt8 {\n            margin-top: 8px\n        }\n\n        .mt12 {\n            margin-top: 12px\n        }\n\n        .mt16 {\n            margin-top: 16px\n        }\n\n        .mt20 {\n            margin-top: 20px\n        }\n\n        .out {\n            background: #f8fafc;\n            border: 1px solid #e2e8f0;\n            padding: 12px;\n            border-radius: 12px;\n            font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n            font-size: .9rem\n        }\n\n        .pill {\n            display: inline-block;\n            background: #ecfdf5;\n            color: #065f46;\n            border: 1px solid #a7f3d0;\n            border-radius: 999px;\n            padding: 4px 10px;\n            font-size: .85rem\n        }\n\n        .right {\n            display: flex;\n            gap: 8px;\n            align-items: center\n        }\n\n        .maintenance-box {\n            margin-top: 14px;\n            background: #fff7ed;\n            border: 1px solid #fdba74;\n            border-left: 6px solid #f97316;\n            color: #9a3412;\n            padding: 14px;\n            border-radius: 14px;\n        }\n\n        .maintenance-box h3 {\n            margin: 0 0 8px 0;\n            font-size: 1rem;\n            color: #9a3412;\n        }\n\n        .maintenance-box p {\n            margin: 6px 0;\n            line-height: 1.6;\n        }\n\n        .maintenance-badge {\n            display: inline-block;\n            background: #ef4444;\n            color: #fff;\n            font-size: .78rem;\n            font-weight: 700;\n            padding: 4px 10px;\n            border-radius: 999px;\n            margin-left: 8px;\n            vertical-align: middle;\n        }\n\n        .tool-disabled {\n            opacity: .55;\n            pointer-events: none;\n            filter: grayscale(.15);\n        }\n\n        .maintenance-link {\n            color: #b45309;\n            font-weight: 700;\n            text-decoration: none;\n        }\n\n        .maintenance-link:hover {\n            text-decoration: underline;\n        }\n    <\/style>\n<\/head>\n\n<body>\n    <div class=\"wrap\">\n        <img decoding=\"async\" class=\"logo\" src=\"https:\/\/abatablaster.xyz\/wp-content\/uploads\/2025\/05\/photo_2024-05-16_15-47-59.jpg\"\n            alt=\"Abata Blaster Logo\" \/>\n        <div class=\"card\">\n            <div class=\"row\" style=\"justify-content:space-between\">\n                <h1>\ud83c\udfa5 Smart Grabber (Video \u2192 Nombor \u2192 CSV)<\/h1>\n                <span id=\"loginState\" class=\"pill\">Semak log masuk\u2026<\/span>\n            <\/div>\n\n            <div id=\"guard\" class=\"muted mt8\">Anda perlu log masuk untuk guna Smart Grabber.<\/div>\n\n            <div id=\"tool\" style=\"display:none\">\n                <div class=\"maintenance-box\">\n                    <h3>\ud83d\udea7 Smart Grabber Under Maintenance <span class=\"maintenance-badge\">MAINTENANCE<\/span><\/h3>\n                    <p>Fungsi ini <b>tidak stabil buat masa sekarang<\/b> dan sedang dalam proses penambahbaikan.<\/p>\n                    <p>Sila <b>DM admin<\/b> jika ada persoalan atau perlukan bantuan lanjut.<\/p>\n                    <p>\u2705 <b>Alternatif:<\/b> Anda masih boleh grab <b>group member<\/b> dan <b>chatlist<\/b> menggunakan <b>Abata Extension Support<\/b>.<\/p>\n                    <p>\ud83d\udcfa Tutorial rujukan:\n                        <a class=\"maintenance-link\" href=\"https:\/\/t.me\/+QHowfJn35qQxYTdl\" target=\"_blank\" rel=\"noopener\">\n                            Buka channel tutorial\n                        <\/a>\n                    <\/p>\n                <\/div>\n\n                <div id=\"toolBody\" class=\"tool-disabled\">\n                    <div class=\"dropzone\" id=\"dz\">\n                        <b>Letak fail video di sini<\/b> atau klik untuk pilih.\n                        <br \/><span class=\"muted\">Sokongan: mp4, mov, webm, mkv, avi \u00b7 Had server 600MB<\/span>\n                        <input id=\"file\" type=\"file\" accept=\"video\/*\" style=\"display:none\">\n                    <\/div>\n                <div class=\"grid mt16\">\n                    <div>\n                        <label>FPS (sampling)<\/label>\n                        <select id=\"fpsSel\">\n                            <option value=\"2\">2 \u2013 Pantas (cepat, kasar)<\/option>\n                            <option value=\"3\" selected>3 \u2013 Seimbang (disyorkan)<\/option>\n                            <option value=\"5\">5 \u2013 Lebih tepat<\/option>\n                            <option value=\"8\">8 \u2013 Sangat tepat (perlahan)<\/option>\n                            <option value=\"custom\">Custom\u2026<\/option>\n                        <\/select>\n                        <div id=\"fpsCustomWrap\" class=\"mt8\" style=\"display:none\">\n                            <input id=\"fpsCustom\" type=\"number\" min=\"1\" max=\"15\" value=\"5\">\n                            <div class=\"muted mt8\">Cadangan: 2\u20138. Nilai lebih tinggi \u2192 lebih tepat, tapi lebih lama.\n                            <\/div>\n                        <\/div>\n                    <\/div>\n                    <div>\n                        <label>Min OCR Confidence<\/label>\n                        <select id=\"minConfSel\">\n                            <option value=\"50\">50 \u2013 Sensitif (tangkap lebih, bunyi lebih)<\/option>\n                            <option value=\"60\" selected>60 \u2013 Seimbang (disyorkan)<\/option>\n                            <option value=\"70\">70 \u2013 Ketat<\/option>\n                            <option value=\"80\">80 \u2013 Sangat ketat<\/option>\n                            <option value=\"custom\">Custom\u2026<\/option>\n                        <\/select>\n                        <div id=\"minConfCustomWrap\" class=\"mt8\" style=\"display:none\">\n                            <input id=\"min_conf_custom\" type=\"number\" min=\"0\" max=\"100\" value=\"65\">\n                            <div class=\"muted mt8\">Julat 0\u2013100. Lebih tinggi = kurang noise, tapi mungkin terlepas\n                                nombor kabur.<\/div>\n                        <\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"grid mt16\">\n                    <div>\n                        <label>ROI (x,y,w,h dalam %)<\/label>\n                        <select id=\"roiPreset\">\n                            <option value=\"8,20,84,68\" selected>Tengah Senarai (default)<\/option>\n                            <option value=\"0,10,100,80\">Penuh Senarai<\/option>\n                            <option value=\"0,15,55,70\">Kiri Senarai<\/option>\n                            <option value=\"45,15,55,70\">Kanan Senarai<\/option>\n                            <option value=\"custom\">Custom\u2026<\/option>\n                        <\/select>\n                        <div id=\"roiCustomWrap\" class=\"mt8\" style=\"display:none\">\n                            <input id=\"roiCustom\" type=\"text\" value=\"8,20,84,68\" placeholder=\"cth 8,20,84,68\"\n                                pattern=\"^\\\\d{1,3},\\\\d{1,3},\\\\d{1,3},\\\\d{1,3}$\">\n                            <div class=\"muted mt8\">Format: x,y,w,h (%). Min 0,0,1,1 \u00b7 Max 100,100,100,100.<\/div>\n                        <\/div>\n                    <\/div>\n                    <div>\n                        <label>Negara (normalisasi)<\/label>\n                        <select id=\"countrySel\">\n                            <option value=\"INTL\" selected>AUTO \u2013 campur MY\/SG\/ID\/BN (jangan paksa +60)<\/option>\n                            <option value=\"MY\">Malaysia (+60)<\/option>\n                            <option value=\"SG\">Singapore (+65)<\/option>\n                            <option value=\"ID\">Indonesia (+62)<\/option>\n                            <option value=\"BN\">Brunei (+673)<\/option>\n                        <\/select>\n                        <div class=\"muted mt8\">Jika senarai campur beberapa negara, pilih \u201cAUTO\u201d.<\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"mt16 row\">\n                    <button class=\"btn\" id=\"goBtn\">Upload &#038; Process<\/button>\n                    <button class=\"btn btn-outline\" id=\"cancelBtn\" disabled>Cancel<\/button>\n                    <div class=\"right muted\" id=\"fileInfo\">Tiada fail dipilih.<\/div>\n                <\/div>\n\n                <div class=\"mt12\">\n                    <div class=\"muted\">Upload<\/div>\n                    <div class=\"bar\">\n                        <div id=\"progUp\"><\/div>\n                    <\/div>\n                <\/div>\n                <div class=\"mt12\">\n                    <div class=\"muted\">Processing (OCR)<\/div>\n                    <div class=\"bar\">\n                        <div id=\"progProc\"><\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"out mt16\" id=\"result\" style=\"display:none\"><\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <script>\n\n        let currentXhr = null, currentJobId = null, currentSSE = null;\n        \/\/ === Firebase setup (sama seperti projek anda) ===\n        const firebaseConfig = {\n            apiKey: \"AIzaSyBou8nlJ7uPZ4ioOJapzC8Dn3-K7Qs-yco\",\n            authDomain: \"whatsapp-ai-saas.firebaseapp.com\",\n            projectId: \"whatsapp-ai-saas\",\n            storageBucket: \"whatsapp-ai-saas.firebasestorage.app\",\n            messagingSenderId: \"287462007544\",\n            appId: \"1:287462007544:web:913db884edf906a37cd10d\"\n        };\n        if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);\n        const auth = firebase.auth();\n\n        \/\/ === UI helpers ===\n        const el = (id) => document.getElementById(id);\n\n        \/\/ --- Helpers untuk baca nilai akhir (ambil dari dropdown\/preset atau custom) ---\n        function resolveFPS() {\n            const sel = el('fpsSel').value;\n            return sel === 'custom' ? (el('fpsCustom').value || '3') : sel;\n        }\n        function resolveMinConf() {\n            const sel = el('minConfSel').value;\n            return sel === 'custom' ? (el('min_conf_custom').value || '60') : sel;\n        }\n        function resolveROI() {\n            const sel = el('roiPreset').value;\n            return sel === 'custom' ? (el('roiCustom').value || '8,20,84,68') : sel;\n        }\n        function resolveCountry() {\n            return el('countrySel').value || 'INTL';\n        }\n\n        \/\/ --- Tunjuk\/sembunyi \u201cCustom\u2026\u201d ---\n        ['fpsSel', 'minConfSel', 'roiPreset'].forEach(id => {\n            const s = el(id);\n            if (!s) return;\n            s.addEventListener('change', () => {\n                if (id === 'fpsSel') el('fpsCustomWrap').style.display = s.value === 'custom' ? '' : 'none';\n                if (id === 'minConfSel') el('minConfCustomWrap').style.display = s.value === 'custom' ? '' : 'none';\n                if (id === 'roiPreset') el('roiCustomWrap').style.display = s.value === 'custom' ? '' : 'none';\n            });\n        });\n\n        \/\/Helpers ke 2\n\n        function setLoginPill(text, ok) {\n            const pill = el('loginState');\n            pill.textContent = text;\n            pill.style.background = ok ? '#ecfdf5' : '#fff7ed';\n            pill.style.color = ok ? '#065f46' : '#9a3412';\n            pill.style.border = '1px solid ' + (ok ? '#a7f3d0' : '#fed7aa');\n        }\n        function bytes(n) { if (n < 1024) return n.toFixed(0) + ' B'; if (n < 1024 * 1024) return (n \/ 1024).toFixed(1) + ' KB'; return (n \/ 1024 \/ 1024).toFixed(1) + ' MB'; }\n\n        function applyMaintenanceMode() {\n            const goBtn = el('goBtn');\n            const cancelBtn = el('cancelBtn');\n            const fileInput = el('file');\n            const toolBody = el('toolBody');\n\n            if (toolBody) toolBody.classList.add('tool-disabled');\n            if (goBtn) {\n                goBtn.disabled = true;\n                goBtn.textContent = '\ud83d\udea7 Under Maintenance';\n            }\n            if (cancelBtn) cancelBtn.disabled = true;\n            if (fileInput) fileInput.disabled = true;\n\n            if (!sessionStorage.getItem('smartGrabberMaintenanceShown')) {\n                sessionStorage.setItem('smartGrabberMaintenanceShown', '1');\n                Swal.fire({\n                    icon: 'warning',\n                    title: 'Smart Grabber Under Maintenance',\n                    html: `\n                        Fungsi ini <b>tidak stabil buat masa sekarang<\/b> dan sedang under maintenance.<br><br>\n                        Sila <b>DM admin<\/b> jika ada persoalan.<br><br>\n                        <b>Alternatif:<\/b> guna <b>Abata Extension Support<\/b> untuk grab <b>group member<\/b> dan <b>chatlist<\/b>.<br><br>\n                        \ud83d\udcfa <a href=\"https:\/\/t.me\/+QHowfJn35qQxYTdl\" target=\"_blank\" rel=\"noopener\">Buka channel tutorial<\/a>\n                    `,\n                    confirmButtonText: 'Faham'\n                });\n            }\n        }\n\n        \/\/ === Auth gate ===\n        auth.onAuthStateChanged((user) => {\n            if (user) {\n                setLoginPill('Log masuk \u2713', true);\n                el('guard').style.display = 'none';\n                el('tool').style.display = '';\n                applyMaintenanceMode();\n            } else {\n                setLoginPill('Belum log masuk', false);\n                el('guard').innerHTML = 'Sila <a href=\"https:\/\/abatablaster.xyz\/index.php\/login\/\">log masuk<\/a> untuk gunakan Smart Grabber.';\n                el('guard').style.display = '';\n                el('tool').style.display = 'none';\n            }\n        });\n\n        \/\/ === Dropzone ===\n        const dz = el('dz'), fileInput = el('file');\n        dz.addEventListener('click', () => fileInput.click());\n        dz.addEventListener('dragover', (e) => { e.preventDefault(); dz.style.background = '#eef9f1'; });\n        dz.addEventListener('dragleave', () => { dz.style.background = '#f7fbf7'; });\n        dz.addEventListener('drop', (e) => {\n            e.preventDefault(); dz.style.background = '#f7fbf7';\n            if (e.dataTransfer.files && e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]);\n        });\n        fileInput.addEventListener('change', () => {\n            if (fileInput.files[0]) setFile(fileInput.files[0]);\n        });\n        function setFile(f) {\n            if (!\/(\\.mp4|\\.mov|\\.webm|\\.mkv|\\.avi)$\/i.test(f.name)) {\n                Swal.fire('Jenis fail tidak disokong', 'Sila pilih video mp4\/mov\/webm\/mkv\/avi.', 'error'); return;\n            }\n            el('fileInfo').textContent = `Fail dipilih: ${f.name} (${bytes(f.size)})`;\n            dz.querySelector('b').textContent = f.name;\n        }\n\n        \/\/ === Upload + progress (XHR supaya nampak progress) ===\n        el('goBtn').addEventListener('click', async () => {\n            const f = fileInput.files[0];\n            if (!f) { Swal.fire('Tiada fail', 'Sila pilih video dahulu.', 'warning'); return; }\n\n            \/\/ Reset UI\n            el('progUp').style.width = '0%';\n            el('progProc').style.width = '0%';\n            el('result').style.display = 'none';\n            el('goBtn').disabled = true; el('goBtn').textContent = 'Memuat naik & memproses\u2026';\n            el('cancelBtn').disabled = false;\n\n            \/\/ 1) Upload \u2192 \/upload2 (dengan XHR agar ada progress)\n            const fd = new FormData();\n            fd.append('video', f);\n            fd.append('fps', resolveFPS());\n            fd.append('roi', resolveROI());\n            fd.append('min_conf', resolveMinConf());\n            fd.append('country', resolveCountry());\n\n            const xhr = new XMLHttpRequest();\n            currentXhr = xhr;\n            xhr.open('POST', '\/smart-grabber\/api\/grabber\/upload2', true);\n\n            xhr.upload.onprogress = (e) => {\n                if (e.lengthComputable) {\n                    const pct = Math.min(100, (e.loaded \/ e.total) * 100);\n                    el('progUp').style.width = pct + '%';\n                }\n            };\n\n            xhr.onreadystatechange = () => {\n                if (xhr.readyState === 4) {\n                    if (xhr.status !== 200) {\n                        el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                        el('cancelBtn').disabled = true;\n                        el('result').style.display = ''; el('result').innerHTML = `\u274c <b>Ralat:<\/b> ${xhr.responseText || 'Upload gagal'}`;\n                        return;\n                    }\n                    try {\n                        const j = JSON.parse(xhr.responseText);\n                        if (!j.ok) throw new Error(j.error || 'Upload gagal');\n                        currentJobId = j.jobId;\n\n                        \/\/ 2) Sambung SSE untuk processing progress\n                        if (currentSSE) { try { currentSSE.close(); } catch { } }\n                        currentSSE = new EventSource(`\/smart-grabber\/api\/grabber\/progress\/${currentJobId}`);\n\n                        currentSSE.addEventListener('start', (ev) => {\n                            const data = JSON.parse(ev.data || '{}');\n                            el('progProc').style.width = '0%';\n                        });\n\n                        currentSSE.addEventListener('progress', (ev) => {\n                            const data = JSON.parse(ev.data || '{}');\n                            if (typeof data.percent === 'number') {\n                                el('progProc').style.width = data.percent + '%';\n                            }\n                        });\n\n                        currentSSE.addEventListener('done', (ev) => {\n                            const data = JSON.parse(ev.data || '{}');\n                            el('progProc').style.width = '100%';\n                            el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                            el('cancelBtn').disabled = true;\n\n                            const csvUrl = '\/smart-grabber' + String(data.csv_url || '').replace(\/^\\\/?results\\\/\/, '\/results\/');\n                            el('result').style.display = '';\n                            el('result').innerHTML = `\u2705 <b>Siap<\/b> \u2014 dijumpai <b>${data.total ?? '-'}<\/b> nombor unik.<br>\ud83d\udcc4 CSV: <a href=\"${csvUrl}\" target=\"_blank\" rel=\"noopener\">${csvUrl}<\/a>`;\n                            try { currentSSE.close(); } catch { } currentSSE = null; currentJobId = null; currentXhr = null;\n                        });\n\n                        currentSSE.addEventListener('cancelled', () => {\n                            el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                            el('cancelBtn').disabled = true;\n                            el('result').style.display = ''; el('result').innerHTML = '\u23f9\ufe0f Proses dibatalkan.';\n                            try { currentSSE.close(); } catch { } currentSSE = null; currentJobId = null; currentXhr = null;\n                        });\n\n                        currentSSE.addEventListener('error', (ev) => {\n                            el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                            el('cancelBtn').disabled = true;\n                            el('result').style.display = ''; el('result').innerHTML = '\u274c Ralat backend \/ server restart. Sila cuba semula.';\n                            try { currentSSE.close(); } catch { } currentSSE = null; currentJobId = null; currentXhr = null;\n                        });\n\n                    } catch (e) {\n                        el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                        el('cancelBtn').disabled = true;\n                        el('result').style.display = ''; el('result').innerHTML = `\u274c <b>Ralat:<\/b> ${e.message}`;\n                    }\n                }\n            };\n\n            xhr.onerror = () => {\n                el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n                el('cancelBtn').disabled = true;\n                el('result').style.display = ''; el('result').innerHTML = '\u274c <b>Ralat rangkaian semasa upload.<\/b>';\n            };\n\n            xhr.send(fd);\n        });\n\n        \/\/ Cancel button\n        el('cancelBtn').addEventListener('click', async () => {\n            try {\n                if (currentXhr) { currentXhr.abort(); }\n                if (currentJobId) {\n                    await fetch(`\/smart-grabber\/api\/grabber\/cancel\/${currentJobId}`, { method: 'POST' });\n                }\n            } catch { }\n            el('cancelBtn').disabled = true;\n            el('goBtn').disabled = false; el('goBtn').textContent = 'Upload & Process';\n            el('result').style.display = ''; el('result').innerHTML = '\u23f9\ufe0f Dibatalkan oleh pengguna.';\n            try { if (currentSSE) currentSSE.close(); } catch { } currentSSE = null; currentJobId = null; currentXhr = null;\n        });\n\n    <\/script>\n<\/body>\n\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Smart Grabber (Video \u2192 CSV) \u2013 AbataBlaster \ud83c\udfa5 Smart Grabber (Video \u2192 Nombor \u2192 CSV) Semak log masuk\u2026 Anda perlu log masuk untuk guna Smart Grabber. \ud83d\udea7 Smart Grabber Under Maintenance MAINTENANCE Fungsi ini tidak stabil buat masa sekarang dan sedang dalam proses penambahbaikan. Sila DM admin jika ada persoalan atau perlukan bantuan lanjut. \u2705 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1056","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1056","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/comments?post=1056"}],"version-history":[{"count":5,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1056\/revisions"}],"predecessor-version":[{"id":1378,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1056\/revisions\/1378"}],"wp:attachment":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/media?parent=1056"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}