/** * 🛰️ UNIFIED CF PAGES TOOLKIT (v18 — Delete Project + Pages Manager + CF DNS Sync) */ const CF_API = "https://api.cloudflare.com/client/v4"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, X-Auth-Email, X-Auth-Key", }; const TEMPLATES = { v1: [ "export default {", " async fetch(request, env) {", " let url = new URL(request.url);", " if (url.pathname.startsWith('/')) {", " const arrStr = [", "__BACKENDS__", " ];", " url.protocol = 'https:';", " url.hostname = getRandomArray(arrStr);", " let new_request = new Request(url, request);", " return fetch(new_request);", " }", " return env.ASSETS.fetch(request);", " }", "};", "function getRandomArray(array) {", " const randomIndex = Math.floor(Math.random() * array.length);", " return array[randomIndex];", "}", ].join("\n"), v2: [ "export default {", " async fetch(request, env, ctx) {", " let url = new URL(request.url);", " let domainBackend = [", "__BACKENDS__", " ];", " if(env.HOST) domainBackend = await ADD(env.HOST);", " let pathUji = env.PATH || '/';", " if (pathUji.charAt(0) !== '/') pathUji = '/' + pathUji;", " let kodeRespon = env.CODE || '200';", " async function getValidResponse(request, hosts) {", " let shuffledHosts = hosts.sort(function() { return Math.random() - 0.5; });", " for (const host of shuffledHosts) {", " let testUrl = new URL(url.href);", " testUrl.hostname = host;", " testUrl.pathname = pathUji.split('?')[0];", " testUrl.search = pathUji.split('?')[1] || '';", " try {", " const response = await fetch(testUrl.href, { method: 'HEAD', redirect: 'manual' });", " if (response.status.toString() === kodeRespon || response.status === 302) {", " let finalUrl = new URL(url.href);", " finalUrl.hostname = host;", " const newHeaders = new Headers(request.headers);", " newHeaders.set('Host', host);", " return await fetch(new Request(finalUrl.href, { method: request.method, headers: newHeaders, body: request.body, redirect: 'follow' }));", " }", " } catch (error) {}", " }", " return new Response('Down', { status: 503 });", " }", " return await getValidResponse(request, domainBackend);", " }", "};", "async function ADD(envadd) {", " var addtext = envadd.replace(/[\\t |\"'`\\r\\n]+/g, ',').replace(/,+/g, ',');", " if (addtext.charAt(0) == ',') addtext = addtext.slice(1);", " if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, -1);", " return addtext.split(',');", "}", ].join("\n"), v3: [ "const WORKER_URLS = [", "__BACKENDS__", "];", "const UA_LIST = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36'];", "async function fetchWithTimeout(resource, options) {", " options = options || {};", " const timeout = options.timeout || 8000;", " const controller = new AbortController();", " const id = setTimeout(function() { controller.abort(); }, timeout);", " try { return await fetch(resource, Object.assign({}, options, { signal: controller.signal })); }", " finally { clearTimeout(id); }", "}", "export default {", " async fetch(request, env, ctx) {", " const url = new URL(request.url);", " const shuffled = WORKER_URLS.slice().sort(function() { return Math.random() - 0.5; });", " for (const host of shuffled) {", " try {", " const targetUrl = new URL(url.href);", " targetUrl.hostname = host;", " targetUrl.protocol = 'https:';", " const headers = new Headers(request.headers);", " headers.set('Host', host);", " headers.set('User-Agent', UA_LIST[0]);", " const newRequest = new Request(targetUrl.href, { method: request.method, headers: headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : null, redirect: 'manual' });", " const response = await fetchWithTimeout(newRequest);", " if (response.status !== 429 && response.status < 500) return response;", " } catch (e) { continue; }", " }", " return new Response('All backends down', { status: 503 });", " }", "};", ].join("\n"), v4: [ "const WORKER_URLS = [", "__BACKENDS__", "];", "export default {", " async fetch(request, env, ctx) {", " const shuffled = WORKER_URLS.slice().sort(function() { return Math.random() - 0.5; });", " for (const host of shuffled) {", " try {", " const targetUrl = new URL(request.url);", " targetUrl.hostname = host;", " targetUrl.protocol = 'https:';", " const response = await fetch(new Request(targetUrl.toString(), { method: request.method, headers: request.headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : null, redirect: 'manual' }));", " if (response.status !== 429 && response.status < 500) return response;", " } catch (e) { continue; }", " }", " return new Response('All workers down', { status: 503 });", " }", "};", ].join("\n"), v5: [ "const CONFIG = {", " TARGETS: [", "__BACKENDS__", " ],", " TIMEOUT_MS: 5000,", " ENABLE_CORS: true", "};", "export default {", " async fetch(request, env, ctx) {", " if (CONFIG.ENABLE_CORS && request.method === 'OPTIONS') {", " return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Max-Age': '86400' } });", " }", " const shuffledTargets = CONFIG.TARGETS.slice().sort(function() { return Math.random() - 0.5; });", " const clientIP = request.headers.get('CF-Connecting-IP');", " for (const host of shuffledTargets) {", " try {", " const url = new URL(request.url);", " url.hostname = host;", " url.protocol = 'https:';", " const headers = new Headers(request.headers);", " headers.set('Host', host);", " if (clientIP) headers.set('X-Forwarded-For', clientIP);", " const controller = new AbortController();", " const timeoutId = setTimeout(function() { controller.abort(); }, CONFIG.TIMEOUT_MS);", " const response = await fetch(new Request(url.toString(), { method: request.method, headers: headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : null, redirect: 'manual', signal: controller.signal }));", " clearTimeout(timeoutId);", " if (response.status < 500 && response.status !== 429) {", " if (CONFIG.ENABLE_CORS) {", " const nh = new Headers(response.headers);", " nh.set('Access-Control-Allow-Origin', '*');", " return new Response(response.body, { status: response.status, headers: nh });", " }", " return response;", " }", " } catch (err) { continue; }", " }", " return new Response(JSON.stringify({ status: 503, error: 'All servers down' }), { status: 503, headers: { 'Content-Type': 'application/json' } });", " }", "};", ].join("\n"), }; const DEFAULT_TEMPLATE_LINES = [ "export default {", " async fetch(request, env, ctx) {", " let url = new URL(request.url);", " let domainBackend = ['payload.goku25.workers.dev'];", " if(env.HOST) domainBackend = await ADD(env.HOST);", " let pathUji = env.PATH || '/';", " if (pathUji.charAt(0) !== '/') pathUji = '/' + pathUji;", " let kodeRespon = env.CODE || '200';", " async function getValidResponse(request, hosts) {", " let shuffledHosts = hosts.sort(function() { return Math.random() - 0.5; });", " for (const host of shuffledHosts) {", " let testUrl = new URL(url.href);", " testUrl.hostname = host;", " testUrl.pathname = pathUji.split('?')[0];", " testUrl.search = pathUji.split('?')[1] || '';", " try {", " const response = await fetch(testUrl.href, { method: 'HEAD', redirect: 'manual' });", " if (response.status.toString() === kodeRespon || response.status === 302) {", " let finalUrl = new URL(url.href);", " finalUrl.hostname = host;", " return await fetch(new Request(finalUrl.href, { method: request.method, headers: new Headers(request.headers), body: request.body, redirect: 'follow' }));", " }", " } catch (error) {}", " }", " return new Response('Down', { status: 503 });", " }", " return await getValidResponse(request, domainBackend);", " }", "};", "async function ADD(envadd) {", " var addtext = envadd.replace(/[\\t |\"'`\\r\\n]+/g, ',').replace(/,+/g, ',');", " if (addtext.charAt(0) == ',') addtext = addtext.slice(1);", " if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, -1);", " return addtext.split(',');", "}", ].join("\n"); export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); if (url.pathname === "/deploy-pages" && request.method === "POST") return handleDeployPages(request); if (url.pathname.startsWith("/api/")) { const apiPath = url.pathname.replace("/api/", "/"); const apiUrl = CF_API + apiPath + url.search; try { const init = { method: request.method, headers: { "X-Auth-Email": request.headers.get("X-Auth-Email") || "", "X-Auth-Key": request.headers.get("X-Auth-Key") || "", "Content-Type": "application/json", }, }; if (request.method !== "GET" && request.method !== "DELETE") init.body = await request.text(); const response = await fetch(apiUrl, init); const data = await response.text(); return new Response(data, { status: response.status, headers: Object.assign({}, corsHeaders, { "Content-Type": "application/json" }) }); } catch (err) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: corsHeaders }); } } return new Response(htmlDashboard(), { headers: { "Content-Type": "text/html; charset=utf-8" } }); }, }; async function handleDeployPages(request) { try { const body = await request.json(); const { email, apiKey, projectName, workerJs, backends, useCustomScript } = body; if (!email || !apiKey) throw new Error("Email & API Key required"); if (!projectName) throw new Error("projectName required"); const result = { success: true, steps: [], url: null, message: "" }; result.steps.push("Getting account..."); const accRes = await fetch(CF_API + "/accounts", { headers: { "X-Auth-Email": email, "X-Auth-Key": apiKey } }); const accData = await accRes.json(); if (!accData.success || !accData.result.length) throw new Error("Invalid credentials"); const accountId = accData.result[0].id; result.steps.push("Account: " + accData.result[0].name); let script = ""; if (useCustomScript && workerJs) { script = workerJs; result.steps.push("Using CUSTOM script (" + script.length + " bytes)"); } if (!script) { script = DEFAULT_TEMPLATE_LINES; if (backends && backends.length > 0) { const arr = backends.map(function (b) { return "'" + b.trim() + "'"; }).join(", "); script = script.replace(/let domainBackend = \['payload\.goku25\.workers\.dev'\];/, "let domainBackend = [" + arr + "];"); } } result.steps.push("Checking project..."); const checkRes = await fetch(CF_API + "/accounts/" + accountId + "/pages/projects/" + projectName, { headers: { "X-Auth-Email": email, "X-Auth-Key": apiKey } }); if (!(await checkRes.json()).success) { await fetch(CF_API + "/accounts/" + accountId + "/pages/projects", { method: "POST", headers: { "X-Auth-Email": email, "X-Auth-Key": apiKey, "Content-Type": "application/json" }, body: JSON.stringify({ name: projectName, production_branch: "main" }), }); } result.steps.push("Deploying..."); const boundary = "----Boundary" + Date.now().toString(36); let body2 = "--" + boundary + '\r\nContent-Disposition: form-data; name="manifest"\r\n\r\n{"name":"' + projectName + '"}\r\n'; body2 += "--" + boundary + '\r\nContent-Disposition: form-data; name="_worker.js"; filename="_worker.js"\r\nContent-Type: application/javascript\r\n\r\n' + script + "\r\n"; body2 += "--" + boundary + "--\r\n"; const deployRes = await fetch(CF_API + "/accounts/" + accountId + "/pages/projects/" + projectName + "/deployments", { method: "POST", headers: { "X-Auth-Email": email, "X-Auth-Key": apiKey, "Content-Type": "multipart/form-data; boundary=" + boundary }, body: body2, }); const deployData = await deployRes.json(); if (deployData.success) { result.url = "https://" + projectName + ".pages.dev"; result.message = "Deployed!"; } else { result.success = false; result.message = JSON.stringify(deployData.errors); } return new Response(JSON.stringify(result), { status: 200, headers: Object.assign({}, corsHeaders, { "Content-Type": "application/json" }) }); } catch (err) { return new Response(JSON.stringify({ success: false, message: err.message }), { status: 500, headers: Object.assign({}, corsHeaders, { "Content-Type": "application/json" }) }); } } function htmlDashboard() { const templatesB64 = btoa(unescape(encodeURIComponent(JSON.stringify(TEMPLATES)))); const defaultB64 = btoa(unescape(encodeURIComponent(DEFAULT_TEMPLATE_LINES))); const HTML_TOP = ` Unified CF Pages Toolkit

UNIFIED CF TOOLKIT

TEMPLATE: DEFAULT
`; const SCRIPT_OPEN = ""; const SCRIPT_CLOSE = ""; const SCRIPT_BODY = SCRIPT_OPEN + ` var TEMPLATES_B64 = "${templatesB64}"; var DEFAULT_B64 = "${defaultB64}"; function b64d(s) { return decodeURIComponent(escape(atob(s))); } var TEMPLATES = JSON.parse(b64d(TEMPLATES_B64)); var DEFAULT_SCRIPT = b64d(DEFAULT_B64); var accounts = JSON.parse(localStorage.getItem('cf_accounts') || '[]'); var showEmailStatus = false; var currentIdx = 0; var selectedDomains = new Set(); var domainListData = []; var currentProject = null; var dnsConfig = JSON.parse(localStorage.getItem('cf_dns_config') || 'null') || { email: '', apiKey: '', zoneId: null, zoneName: '', autoSync: false, ttl: 1, cachedZones: [] }; var currentTemplate = localStorage.getItem('cf_tpl') || 'default'; var deployMode = localStorage.getItem('cf_deploy_mode') || 'template'; var sidebarClosed = localStorage.getItem('cf_sidebar') === 'closed'; function saveDnsConfig() { localStorage.setItem('cf_dns_config', JSON.stringify(dnsConfig)); } function applySidebarState() { document.body.classList.toggle('sb-closed', sidebarClosed); } function toggleSidebar() { sidebarClosed = !sidebarClosed; localStorage.setItem('cf_sidebar', sidebarClosed ? 'closed' : 'open'); applySidebarState(); } function switchTab(tab) { document.getElementById('tabDeployer').classList.toggle('hidden', tab !== 'deployer'); document.getElementById('tabDomains').classList.toggle('hidden', tab !== 'domains'); document.getElementById('tabBtnDeployer').classList.toggle('active', tab === 'deployer'); document.getElementById('tabBtnDomains').classList.toggle('active', tab === 'domains'); if (tab === 'deployer') renderDeployerAccountSelect(); } function setDeployMode(mode) { deployMode = mode; localStorage.setItem('cf_deploy_mode', mode); document.getElementById('modeBtnD').classList.toggle('active', mode === 'template'); document.getElementById('modeBtnC').classList.toggle('active', mode === 'custom'); document.getElementById('templatePanel').classList.toggle('hidden', mode !== 'template'); document.getElementById('customPanel').classList.toggle('hidden', mode !== 'custom'); var ta = document.getElementById('scriptEditD'); var hint = document.getElementById('editHint'); if (mode === 'custom') { ta.readOnly = false; ta.style.color = '#a3e635'; hint.textContent = '(editable)'; hint.className = 'text-lime-400'; } else { ta.readOnly = true; ta.style.color = '#fbbf24'; hint.textContent = '(auto-generated)'; hint.className = 'text-amber-400'; loadTemplate(currentTemplate); } } function getBackendsD() { return document.getElementById('backendListD').value .split('\\n').map(function(s) { return s.trim(); }) .filter(function(s) { return s.length > 0 && s.indexOf('.') !== -1; }); } function buildScriptFromTemplate(tpl, backends) { if (tpl === 'default' || !TEMPLATES[tpl]) { var s = DEFAULT_SCRIPT; if (backends.length) { var arr = backends.map(function(b) { return "'" + b + "'"; }).join(", "); s = s.replace(/let domainBackend = \\['payload\\.goku25\\.workers\\.dev'\\];/, "let domainBackend = [" + arr + "];"); } return s; } var tplStr = TEMPLATES[tpl]; var arrLines = backends.map(function(b) { return JSON.stringify(b); }).join(",\\n"); return tplStr.replace("__BACKENDS__", backends.length ? arrLines : ""); } function loadTemplate(tpl) { currentTemplate = tpl; localStorage.setItem('cf_tpl', tpl); if (deployMode !== 'template') return; var be = getBackendsD(); var s = buildScriptFromTemplate(tpl, be); document.getElementById('scriptEditD').value = s; document.querySelectorAll('.tpl-btn').forEach(function(b) { b.classList.toggle('active', b.getAttribute('data-tpl') === tpl); }); document.getElementById('tplStatus').textContent = 'TEMPLATE: ' + tpl.toUpperCase(); } function refreshPreview() { if (deployMode === 'template') loadTemplate(currentTemplate); else alert('Mode CUSTOM: editor bebas, tidak ada refresh preview.'); } function resetDeployScript() { if (deployMode === 'custom') { document.getElementById('scriptEditD').value = ''; } else { loadTemplate(currentTemplate); } } function renderDeployerAccountSelect() { var sel = document.getElementById('accSelectD'); sel.innerHTML = accounts.map(function(acc, i) { return ''; }).join('') || ''; } async function deployPages() { var idx = document.getElementById('accSelectD').value; if (idx === '' || !accounts[idx]) { alert('Pilih akun dulu'); return; } var acc = accounts[idx]; var pn = document.getElementById('deployProjectName').value.trim(); if (!pn) { alert('Isi project name'); return; } var backends = getBackendsD(); var script; if (deployMode === 'custom') { script = document.getElementById('scriptEditD').value; if (!script.trim()) { alert('Script custom kosong'); return; } } else { var badBackend = backends.find(function(b) { return /["'\\\\\\s]/.test(b); }); if (badBackend) { alert('Backend tidak valid: ' + badBackend); return; } script = buildScriptFromTemplate(currentTemplate, backends); } if (!script) { alert('Gagal build script'); return; } var btn = document.getElementById('btnDeployPages'); var out = document.getElementById('deployResult'); btn.disabled = true; btn.textContent = 'DEPLOYING...'; out.classList.remove('hidden'); out.innerHTML = '>> Deploying ' + pn + ' | mode=' + deployMode + ' | tpl=' + (deployMode === 'template' ? currentTemplate : '-') + ' | size=' + script.length + 'B
'; try { var r = await fetch('/deploy-pages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: acc.email, apiKey: acc.key, projectName: pn, workerJs: script, backends: backends, useCustomScript: true }) }); var txt = await r.text(); var d; try { d = JSON.parse(txt); } catch (parseErr) { out.innerHTML += '✗ Non-JSON response (HTTP ' + r.status + '):
' + txt.substring(0, 500).replace(/'; btn.disabled = false; btn.textContent = 'Deploy Pages'; return; } if (d.steps) d.steps.forEach(function(s) { out.innerHTML += '>> ' + s + '
'; }); if (d.success) { out.innerHTML += '✓ SUCCESS: ' + d.url + ''; } else { out.innerHTML += '✗ FAILED: ' + d.message + ''; } } catch (e) { out.innerHTML += '✗ ERROR: ' + e.message + ''; } out.scrollTop = out.scrollHeight; btn.disabled = false; btn.textContent = 'Deploy Pages'; } window.addEventListener('load', function() { try { applySidebarState(); renderDnsPanel(); renderSyncStrip(); renderDeployerAccountSelect(); setDeployMode(deployMode); loadTemplate(currentTemplate); if (accounts.length > 0) { renderAll(); handleMainAccountChange(); } else { var aa = document.getElementById('actionArea'); if (aa) aa.classList.add('hidden'); var es = document.getElementById('emptyState'); if (es) es.classList.remove('hidden'); } document.querySelectorAll('.tpl-btn').forEach(function(b) { b.onclick = function() { loadTemplate(b.getAttribute('data-tpl')); }; }); document.getElementById('backendListD').addEventListener('input', function() { if (deployMode === 'template') loadTemplate(currentTemplate); }); } catch (e) { console.error('Boot error:', e); } }); function toggleBulkModal(show) { document.getElementById('bulkModal').classList.toggle('hidden', !show); } function toggleNcModal(show) { document.getElementById('ncModal').classList.toggle('hidden', !show); } function toggleBulkDeleteModal(show) { if (show && selectedDomains.size === 0) { alert('Pilih domain dulu'); return; } document.getElementById('bulkDeleteModal').classList.toggle('hidden', !show); if (show) updateBulkDeleteModal(); } function toggleShowEmails() { showEmailStatus = !showEmailStatus; var t1 = document.getElementById('eyeText'); if (t1) t1.innerText = showEmailStatus ? 'Hide Email' : 'Show Email'; var i1 = document.getElementById('eyeIcon'); if (i1) i1.innerText = showEmailStatus ? '🙈' : '👁️'; var t2 = document.getElementById('eyeText2'); if (t2) t2.innerText = showEmailStatus ? 'Hide Email' : 'Show Email'; var i2 = document.getElementById('eyeIcon2'); if (i2) i2.innerText = showEmailStatus ? '🙈' : '👁️'; renderAll(); renderDeployerAccountSelect(); renderDnsPanel(); } function maskEmail(email) { if (!email) return ""; var parts = email.split('@'); return parts[0].substring(0, 4) + "****"; } function copyToClipboard(text, label) { navigator.clipboard.writeText(text).then(function() { alert(label + " copied!"); }); } function renderAll() { renderMainSelector(); renderSidebarDetails(); } function renderMainSelector() { var select = document.getElementById('accSelect'); select.innerHTML = accounts.map(function(acc, index) { return ''; }).join(''); } function renderSidebarDetails() { var list = document.getElementById('accountListDetails'); list.innerHTML = accounts.map(function(acc, index) { var activeCls = index == currentIdx ? 'border-indigo-500 ring-1 ring-indigo-500/50' : 'border-slate-800/50'; var labelCls = index == currentIdx ? 'text-indigo-400' : 'text-slate-500'; return '
' + '
' + '' + acc.label + '' + '
' + '' + '' + '' + '
' + '
' + '
' + (showEmailStatus ? acc.email : maskEmail(acc.email)) + '
' + '
'; }).join(''); } function configureDns() { document.getElementById('ncEmail').value = dnsConfig.email || ''; document.getElementById('ncToken').value = dnsConfig.apiKey || ''; document.getElementById('ncAutoSync').checked = !!dnsConfig.autoSync; document.getElementById('ncTtl').value = dnsConfig.ttl || 1; var sel = document.getElementById('ncDomain'); if (dnsConfig.cachedZones && dnsConfig.cachedZones.length) { sel.innerHTML = dnsConfig.cachedZones.map(function(z) { return ''; }).join(''); } else if (dnsConfig.zoneId) { sel.innerHTML = ''; } document.getElementById('ncModalMsg').textContent = ''; toggleNcModal(true); } async function fetchCFZones() { var email = document.getElementById('ncEmail').value.trim(); var key = document.getElementById('ncToken').value.trim(); var msg = document.getElementById('ncModalMsg'); var sel = document.getElementById('ncDomain'); if (!email || !key) { msg.textContent = '⚠️ Email & Global API Key wajib'; return; } msg.textContent = '⏳ Loading Cloudflare zones...'; try { var r = await fetch('/api/zones?per_page=50', { headers: { 'X-Auth-Email': email, 'X-Auth-Key': key } }); var data = await r.json(); if (!data.success) { msg.textContent = '❌ ' + ((data.errors && data.errors[0] && data.errors[0].message) || 'HTTP ' + r.status); return; } var zones = data.result || []; if (!zones.length) { msg.textContent = '⚠️ Tidak ada zone di akun ini'; return; } dnsConfig.cachedZones = zones.map(function(z) { return { id: z.id, name: z.name }; }); sel.innerHTML = zones.map(function(z) { return ''; }).join(''); msg.textContent = '✅ ' + zones.length + ' zone dimuat'; } catch (e) { msg.textContent = '❌ ' + e.message; } } function saveDnsSettings() { var email = document.getElementById('ncEmail').value.trim(); var apiKey = document.getElementById('ncToken').value.trim(); var zoneSel = document.getElementById('ncDomain').value; var autoSync = document.getElementById('ncAutoSync').checked; var ttl = Number(document.getElementById('ncTtl').value) || 1; var msg = document.getElementById('ncModalMsg'); if (!email || !apiKey) { msg.textContent = '⚠️ Email & API Key wajib diisi'; return; } if (!zoneSel) { msg.textContent = '⚠️ Pilih zone dulu'; return; } var parts = zoneSel.split('|'); dnsConfig.email = email; dnsConfig.apiKey = apiKey; dnsConfig.zoneId = parts[0]; dnsConfig.zoneName = parts[1]; dnsConfig.autoSync = autoSync; dnsConfig.ttl = ttl; saveDnsConfig(); renderDnsPanel(); renderSyncStrip(); toggleNcModal(false); } function clearDnsSettings() { if (!confirm('Hapus konfigurasi Cloudflare DNS?')) return; dnsConfig = { email: '', apiKey: '', zoneId: null, zoneName: '', autoSync: false, ttl: 1, cachedZones: [] }; saveDnsConfig(); renderDnsPanel(); renderSyncStrip(); toggleNcModal(false); } function renderDnsPanel() { var box = document.getElementById('ncStatus'); if (!box) return; if (!dnsConfig.email || !dnsConfig.zoneId) { box.innerHTML = '

Belum dikonfigurasi. Klik CONFIGURE.

'; return; } box.innerHTML = '
' + '' + 'Cloudflare DNS' + '
' + '
' + (showEmailStatus ? dnsConfig.email : maskEmail(dnsConfig.email)) + '
' + '
🌐 ' + dnsConfig.zoneName + '
' + ''; } function toggleAutoSync(val) { dnsConfig.autoSync = val; saveDnsConfig(); renderSyncStrip(); } function renderSyncStrip() { var el = document.getElementById('syncStrip'); if (!el) return; if (!dnsConfig.email || !dnsConfig.zoneId) { el.innerHTML = '⚠️ CF DNS belum dikonfigurasi —Configure'; return; } var target = currentProject ? (currentProject.subdomain || (currentProject.name + '.pages.dev')) : '(pilih project)'; var active = dnsConfig.autoSync ? '● AKTIF' : '○ mati'; el.innerHTML = '🌐 CF DNS Zone: ' + dnsConfig.zoneName + '' + '|' + '🎯 Target: ' + target + '' + '|' + 'Auto-sync: ' + active + ''; } function toggleSelectAll() { if (selectedDomains.size === domainListData.length && domainListData.length > 0) { selectedDomains.clear(); } else { domainListData.forEach(function(d) { selectedDomains.add(d.name); }); } updateDomainCheckboxes(); updateBulkDeleteButton(); } function toggleDomainSelection(domainName) { if (selectedDomains.has(domainName)) selectedDomains.delete(domainName); else selectedDomains.add(domainName); updateBulkDeleteButton(); updateSelectAllButton(); } function updateDomainCheckboxes() { document.querySelectorAll('.domain-checkbox').forEach(function(cb) { cb.checked = selectedDomains.has(cb.value); }); updateSelectAllButton(); } function updateSelectAllButton() { var btn = document.getElementById('btnSelectAll'); if (!btn) return; if (domainListData.length === 0) btn.innerText = 'SELECT ALL'; else if (selectedDomains.size === domainListData.length) btn.innerText = 'DESELECT ALL'; else btn.innerText = 'SELECT ALL'; } function updateBulkDeleteButton() { var btn = document.getElementById('btnBulkDelete'); if (!btn) return; if (selectedDomains.size > 0) { btn.classList.remove('hidden'); btn.innerText = 'DELETE SELECTED (' + selectedDomains.size + ')'; } else btn.classList.add('hidden'); } function updateBulkDeleteModal() { document.getElementById('deleteCount').innerText = selectedDomains.size; document.getElementById('deletePreview').innerHTML = Array.from(selectedDomains).map(function(d) { return '
🌐 ' + d + '
'; }).join(''); } async function handleMainAccountChange() { currentIdx = document.getElementById('accSelect').value; selectedDomains.clear(); updateBulkDeleteButton(); renderAll(); await fetchProjects(); } function getBaseHeaders() { var acc = accounts[currentIdx]; return { 'X-Auth-Email': acc.email, 'X-Auth-Key': acc.key }; } async function fetchProjects() { try { var res = await fetch('/api/accounts', { headers: getBaseHeaders() }); var data = await res.json(); if (!data.success) throw new Error((data.errors && data.errors[0] && data.errors[0].message) || 'CF error'); var cloudflareAccountId = data.result[0].id; var projRes = await fetch('/api/accounts/' + cloudflareAccountId + '/pages/projects', { headers: getBaseHeaders() }); var projData = await projRes.json(); window.__cfAccountId = cloudflareAccountId; var projSelect = document.getElementById('projSelect'); if (projData.result && projData.result.length > 0) { projSelect.innerHTML = projData.result.map(function(p) { return ''; }).join(''); document.getElementById('actionArea').classList.remove('hidden'); await fetchDomains(); } else { projSelect.innerHTML = ''; document.getElementById('actionArea').classList.add('hidden'); } } catch (e) { alert("Auth Failure: " + e.message); } } async function fetchDomains() { var sel = document.getElementById('projSelect'); var val = sel.value; if (!val || val === 'No Projects Found') return; var parts = val.split('/'); var accId = parts[0]; var projName = parts[1]; var subdomain = sel.options[sel.selectedIndex].dataset.subdomain || (projName + '.pages.dev'); currentProject = { name: projName, subdomain: subdomain }; renderSyncStrip(); var listEl = document.getElementById('domainList'); listEl.innerHTML = '
PULLING DATA...
'; try { var res = await fetch('/api/accounts/' + accId + '/pages/projects/' + projName + '/domains', { headers: getBaseHeaders() }); var data = await res.json(); domainListData = data.result || []; selectedDomains.clear(); updateBulkDeleteButton(); updateSelectAllButton(); listEl.innerHTML = domainListData.map(function(d) { return '
  • ' + '
    ' + '' + '
    ' + '
    ' + d.name + '
    ' + '
    ' + '' + '' + d.status + '' + '
    ' + '
    ' + '
    ' + '' + '
  • '; }).join('') || '
  • No Domains Active
  • '; } catch (e) { listEl.innerHTML = '
  • CONNECTION ERROR
  • '; domainListData = []; } } async function deleteCurrentProject() { var sel = document.getElementById('projSelect'); var val = sel.value; if (!val || val === 'No Projects Found') { alert('Tidak ada project terpilih'); return; } var parts = val.split('/'); var accId = parts[0]; var projName = parts[1]; if (!confirm('HAPUS PROJECT CF PAGES "' + projName + '"?\\n\\nSemua custom domain yang nempel di project ini akan ikut terhapus.\\nIni TIDAK BISA dibatalkan!')) return; if (!confirm('Konfirmasi sekali lagi: hapus project "' + projName + '"?')) return; try { var dr = await fetch('/api/accounts/' + accId + '/pages/projects/' + projName, { method: 'DELETE', headers: getBaseHeaders() }); var dd = await dr.json(); if (dd.success) { alert('✓ Project "' + projName + '" berhasil dihapus'); await fetchProjects(); document.getElementById('actionArea').classList.add('hidden'); } else { alert('✗ Gagal hapus: ' + JSON.stringify(dd.errors || dd)); } } catch (e) { alert('✗ Error: ' + e.message); } } function matchCFZone(fullDomain) { var zones = dnsConfig.cachedZones || []; var best = null; for (var i = 0; i < zones.length; i++) { var z = zones[i]; if (fullDomain === z.name || fullDomain.endsWith('.' + z.name)) { if (!best || z.name.length > best.name.length) best = z; } } if (!best) return null; var recordName; if (fullDomain === best.name) recordName = '@'; else recordName = fullDomain.slice(0, fullDomain.length - best.name.length - 1); return { zoneId: best.id, zone: best.name, recordName: recordName }; } function dnsHeaders() { return { 'X-Auth-Email': dnsConfig.email, 'X-Auth-Key': dnsConfig.apiKey, 'Content-Type': 'application/json' }; } async function cfAddDnsRecord(fullDomain, target) { var r = await fetch('/api/zones/' + dnsConfig.zoneId + '/dns_records', { method: 'POST', headers: dnsHeaders(), body: JSON.stringify({ type: 'CNAME', name: fullDomain, content: target, ttl: dnsConfig.ttl || 1, proxied: false }) }); var data = await r.json(); return { ok: r.ok && data.success, data: data }; } async function cfDeleteDnsRecordByMatch(fullDomain) { var match = matchCFZone(fullDomain); if (!match) return { ok: false, error: 'no match' }; try { var listRes = await fetch('/api/zones/' + dnsConfig.zoneId + '/dns_records?type=CNAME&per_page=100', { headers: { 'X-Auth-Email': dnsConfig.email, 'X-Auth-Key': dnsConfig.apiKey } }); var listData = await listRes.json(); var records = (listData && listData.result) || []; var rec = records.find(function(r) { return r.type === 'CNAME' && (r.name === fullDomain || (match.recordName === '@' && r.name === match.zone)); }); if (!rec) return { ok: false, error: 'record not found' }; var dr = await fetch('/api/zones/' + dnsConfig.zoneId + '/dns_records/' + rec.id, { method: 'DELETE', headers: { 'X-Auth-Email': dnsConfig.email, 'X-Auth-Key': dnsConfig.apiKey } }); var dd = await dr.json(); return { ok: dr.ok && dd.success }; } catch (e) { return { ok: false, error: e.message }; } } async function handleBulkAdd() { var list = document.getElementById('bulkList').value.split('\\n').map(function(s) { return s.trim(); }).filter(Boolean); var sel = document.getElementById('projSelect'); var parts = sel.value.split('/'); var accId = parts[0]; var projName = parts[1]; var target = sel.options[sel.selectedIndex].dataset.subdomain || (projName + '.pages.dev'); var btn = document.getElementById('btnBulk'); var progress = document.getElementById('progress'); if (list.length === 0) { alert('Daftar kosong'); return; } var syncOn = dnsConfig.autoSync && dnsConfig.email && dnsConfig.apiKey && dnsConfig.zoneId; btn.disabled = true; btn.classList.add('opacity-50'); progress.classList.remove('hidden'); progress.innerHTML = '>> BOOTING SEQUENCE... ' + (syncOn ? '(CF DNS sync ON)' : '(CF DNS sync OFF)') + '
    '; var okCF = 0, okNC = 0, failCF = 0, failNC = 0, skipNC = 0; for (var i = 0; i < list.length; i++) { var domain = list[i].trim(); progress.innerHTML += '>> ' + domain + '
    '; progress.scrollTop = progress.scrollHeight; progress.innerHTML += '  CF Pages ... '; try { var headers = getBaseHeaders(); headers['Content-Type'] = 'application/json'; var res = await fetch('/api/accounts/' + accId + '/pages/projects/' + projName + '/domains', { method: 'POST', headers: headers, body: JSON.stringify({ name: domain }) }); var data = await res.json(); if (data.success) { okCF++; progress.innerHTML += '✓ Success
    '; } else { failCF++; progress.innerHTML += '✗ ' + ((data.errors && data.errors[0] && data.errors[0].message) || 'Fail') + '
    '; } } catch (e) { failCF++; progress.innerHTML += '✗ ' + e.message + '
    '; } progress.scrollTop = progress.scrollHeight; if (syncOn) { progress.innerHTML += '  CF DNS ... '; var match = matchCFZone(domain); if (!match) { skipNC++; progress.innerHTML += '⊘ tidak match zone
    '; } else { try { var r = await cfAddDnsRecord(domain, target); if (r.ok) { okNC++; var rid = r.data && r.data.result ? r.data.result.id : ''; progress.innerHTML += '✓ ' + match.recordName + ' → ' + target + (rid ? ' (#' + rid.substring(0,8) + ')' : '') + '
    '; } else { failNC++; var errMsg = (r.data && r.data.errors && r.data.errors[0] && r.data.errors[0].message) || 'fail'; progress.innerHTML += '✗ ' + errMsg + '
    '; } } catch (e) { failNC++; progress.innerHTML += '✗ ' + e.message + '
    '; } } progress.scrollTop = progress.scrollHeight; } } btn.disabled = false; btn.classList.remove('opacity-50'); progress.innerHTML += '
    >> DONE
    '; progress.innerHTML += 'CF Pages: ✓ ' + okCF + ' ✗ ' + failCF + '
    '; if (syncOn) progress.innerHTML += 'CF DNS: ✓ ' + okNC + ' ✗ ' + failNC + ' ⊘ ' + skipNC + '
    '; progress.scrollTop = progress.scrollHeight; await fetchDomains(); } async function deleteDomain(name) { if (!confirm('Delete ' + name + ' from CF Pages?')) return; var sel = document.getElementById('projSelect'); var parts = sel.value.split('/'); try { var res = await fetch('/api/accounts/' + parts[0] + '/pages/projects/' + parts[1] + '/domains/' + name, { method: 'DELETE', headers: getBaseHeaders() }); var data = await res.json(); if (data.success) { selectedDomains.delete(name); updateBulkDeleteButton(); } } catch (e) { console.error(e); } await fetchDomains(); } async function executeBulkDelete() { var sel = document.getElementById('projSelect'); var parts = sel.value.split('/'); var progress = document.getElementById('progress'); var alsoDns = document.getElementById('alsoNodelocDelete').checked; var domainsToDelete = Array.from(selectedDomains); toggleBulkDeleteModal(false); progress.classList.remove('hidden'); progress.innerHTML = '>> BULK DELETE SEQUENCE...' + (alsoDns ? ' (CF DNS ON)' : '') + '
    '; for (var i = 0; i < domainsToDelete.length; i++) { var domain = domainsToDelete[i]; progress.innerHTML += '>> ' + domain + ' '; try { var res = await fetch('/api/accounts/' + parts[0] + '/pages/projects/' + parts[1] + '/domains/' + domain, { method: 'DELETE', headers: getBaseHeaders() }); var data = await res.json(); progress.innerHTML += data.success ? 'CF ✓ ' : 'CF ✗ '; } catch (e) { progress.innerHTML += 'CF ERR '; } if (alsoDns && dnsConfig.email && dnsConfig.apiKey && dnsConfig.zoneId) { var r = await cfDeleteDnsRecordByMatch(domain); progress.innerHTML += r.ok ? 'DNS ✓' : 'DNS ⊘'; } progress.innerHTML += '
    '; progress.scrollTop = progress.scrollHeight; } selectedDomains.clear(); updateBulkDeleteButton(); await fetchDomains(); progress.innerHTML += '>> BULK DELETE COMPLETED
    '; } function processBulkAccounts() { var raw = document.getElementById('bulkAccountInput').value.trim(); if (!raw) return; var lines = raw.split('\\n').map(function(l) { return l.trim(); }).filter(function(l) { return l !== ''; }); var newAccs = []; for (var i = 0; i < lines.length; i += 2) { var email = lines[i], key = lines[i + 1]; if (email && key) newAccs.push({ label: 'Profile ' + (accounts.length + newAccs.length + 1), email: email, key: key }); } if (newAccs.length > 0) { accounts = accounts.concat(newAccs); localStorage.setItem('cf_accounts', JSON.stringify(accounts)); location.reload(); } } function deleteAccount(idx) { if (!confirm('Hapus profile ini?')) return; accounts.splice(idx, 1); localStorage.setItem('cf_accounts', JSON.stringify(accounts)); location.reload(); } function copyAllAccounts() { var text = accounts.map(function(a) { return a.email + '\\n' + a.key; }).join('\\n\\n'); copyToClipboard(text, 'Vault backup'); } function clearAllData() { if (!confirm('DANGER: Hapus semua akun CF + konfigurasi CF DNS?')) return; localStorage.clear(); location.reload(); } ` + SCRIPT_CLOSE; return HTML_TOP + SCRIPT_BODY + ` `; }