Edit: workermanager
Nama Worker
Kode Sumber
--264a4f5f1706602b218129b0f3f3ba55e1a7abe45544adf3bbdee984c8e9 Content-Disposition: form-data; name="worker.js" // --- KONFIGURASI WAJIB --- const CF_API_TOKEN = "fo1CJsqU979a93ZRiIG2Ysm4LOFHnpTFOGlaTKo2"; const CF_ACCOUNT_ID = "462eb1c0067799de3eea61380d1c1fd9"; // ------------------------- const WORKER_TEMPLATES = { "hello-world": `export default {\n async fetch(request, env, ctx) {\n console.log("Request received for:", request.url);\n return new Response('Hello Bang Ariv Ganteng');\n },\n};`, "json-api": `export default {\n async fetch(request, env, ctx) {\n try {\n const data = { message: "Ini adalah API JSON By Bang Ariv", timestamp: new Date() };\n console.log("Successfully served JSON API request.");\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (e) {\n console.error("Error in JSON API:", e);\n return new Response(e.message, { status: 500 });\n }\n },\n};`, "proxy": `export default {\n async fetch(request, env, ctx) {\n const url = new URL(request.url);\n const targetHost = 'example.com'; // Ganti dengan target proxy Create By Bang Ariv\n console.log(\`Proxying request to \${targetHost}\`);\n url.hostname = targetHost;\n return fetch(new Request(url, request));\n },\n};` }; export default { async fetch(request) { if (CF_API_TOKEN.startsWith("GANTI_") || CF_ACCOUNT_ID.startsWith("GANTI_")) { return new Response("Owalah Goblok : Harap isi CF_API_TOKEN dan CF_ACCOUNT_ID.", { status: 500 }); } const url = new URL(request.url); const pathname = url.pathname; if (request.method === "POST") { const formData = await request.formData(); const handleApiResponse = async (response, successUrl) => { if (response.ok) { try { const result = await response.json(); if (result.success === true || result.success === null) { return Response.redirect(successUrl, 303); } } catch (e) { if (response.status >= 200 && response.status < 300) { return Response.redirect(successUrl, 303); } } } const result = await response.json(); const error = result.errors[0] || { message: "Unknown error", code: "N/A" }; return new Response(`Gagal: ${error.message} (Code: ${error.code})`, { status: response.status }); }; if (pathname === "/api/create-kv") { const title = formData.get("kvTitle"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces`, { method: "POST", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/create-r2") { const name = formData.get("r2Name"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/r2/buckets`, { method: "POST", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/create-d1") { const name = formData.get("d1Name"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database`, { method: "POST", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/delete-kv") { const namespaceId = formData.get("namespaceId"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${namespaceId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/delete-r2") { const bucketName = formData.get("bucketName"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/r2/buckets/${bucketName}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/delete-d1") { const databaseId = formData.get("databaseId"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database/${databaseId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return handleApiResponse(response, url.origin + "?action=storage"); } if (pathname === "/api/kv-put") { const { namespaceId, key, value, title } = Object.fromEntries(formData); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`, { method: "PUT", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "text/plain" }, body: value }); const redirectUrl = `${url.origin}?action=browse-kv&ns_id=${namespaceId}&title=${encodeURIComponent(title)}`; return handleApiResponse(response, redirectUrl); } if (pathname === "/api/kv-delete-key") { const { namespaceId, key, title } = Object.fromEntries(formData); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); const redirectUrl = `${url.origin}?action=browse-kv&ns_id=${namespaceId}&title=${encodeURIComponent(title)}`; return handleApiResponse(response, redirectUrl); } if (pathname === "/api/deploy") { const { scriptName, content, bindings } = Object.fromEntries(formData); let bindingsArray = []; try { bindingsArray = JSON.parse(bindings); if (!Array.isArray(bindingsArray)) throw new Error(); } catch (e) { return new Response('Gagal Deploy: Format bindings JSON tidak valid.', { status: 400 }); } const deployFormData = new FormData(); deployFormData.append('metadata', new Blob([JSON.stringify({ main_module: "worker.js", bindings: bindingsArray })], { type: 'application/json' })); deployFormData.append('worker.js', new Blob([content], { type: 'application/javascript+module' })); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${scriptName}`, { method: "PUT", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` }, body: deployFormData }); return handleApiResponse(response, url.origin); } if (pathname === "/api/delete-script") { const scriptName = formData.get("scriptName"); await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${scriptName}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return Response.redirect(url.origin, 303); } if (pathname === "/api/add-domain") { const { zoneId, hostname, scriptName } = Object.fromEntries(formData); await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/domains`, { method: "PUT", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ hostname, service: scriptName, zone_id: zoneId }), }); return Response.redirect(url.origin, 303); } if (pathname === "/api/delete-domain") { const hostname = formData.get("hostname"); await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/domains/${hostname}`, { method: "DELETE", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return Response.redirect(url.origin, 303); } } const action = url.searchParams.get("action"); const scriptName = url.searchParams.get("script"); if (action === "create" || (action === "edit" && scriptName)) { return handleEditorPage(action, scriptName); } else if (action === "storage") { return handleStoragePage(); } else if (action === "browse-kv") { return handleKvBrowsePage(url); } else if (action === "logs") { return handleLogsPage(url); } else if (action === "get-kv-value") { const { ns_id, key } = Object.fromEntries(url.searchParams); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${ns_id}/values/${encodeURIComponent(key)}`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); return new Response(await response.text(), { status: response.status }); } else { return handleListPage(); } }, }; // --- Handler Functions --- async function handleListPage() { const usageQuery = { query: `query { viewer { accounts(filter: { accountTag: "${CF_ACCOUNT_ID}" }) { workersInvocationsAdaptive(limit: 1, orderBy: [datetime_DESC]) { sum { requests } } } } }` }; const [scriptsResponse, domainsResponse, zonesResponse, usageResponse] = await Promise.all([ fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/domains`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/zones?account.id=${CF_ACCOUNT_ID}`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch("https://api.cloudflare.com/client/v4/graphql", { method: "POST", headers: { "Authorization": `Bearer ${CF_API_TOKEN}` }, body: JSON.stringify(usageQuery) }).then(res => res.json()) ]); const workers = scriptsResponse.result || []; const domains = domainsResponse.result || []; const zones = zonesResponse.result || []; const dailyUsage = usageResponse.data?.viewer?.accounts[0]?.workersInvocationsAdaptive[0]?.sum?.requests || 0; const domainsByWorker = {}; domains.forEach(domain => { domainsByWorker[domain.service] = [...(domainsByWorker[domain.service] || []), domain]; }); return new Response(generateListPageHTML({ workers, domainsByWorker, zones, dailyUsage }), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } async function handleEditorPage(action, scriptName) { let scriptContent = WORKER_TEMPLATES["hello-world"]; let scriptBindings = []; let pageTitle = "Buat Worker Baru"; const [kvResponse, r2Response, d1Response] = await Promise.all([ fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/r2/buckets`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()) ]); const availableResources = { kv: kvResponse.result || [], r2: r2Response.result?.buckets || [], d1: d1Response.result || [] }; if (action === "edit") { pageTitle = `Edit: ${scriptName}`; const [contentResponse, settingsResponse] = await Promise.all([ fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${scriptName}`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Accept": "application/javascript" } }), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${scriptName}/settings`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }) ]); scriptContent = await contentResponse.text(); const settingsResult = await settingsResponse.json(); if (settingsResult.success) { scriptBindings = settingsResult.result.bindings || []; } } return new Response(generateEditorPageHTML({ pageTitle, scriptName, scriptContent, scriptBindings, availableResources }), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } async function handleStoragePage() { const [kvResponse, r2Response, d1Response] = await Promise.all([ fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/r2/buckets`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()), fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }).then(res => res.json()) ]); const kvNamespaces = kvResponse.result || []; const r2Buckets = r2Response.result?.buckets || []; const d1Databases = d1Response.result || []; return new Response(generateStoragePageHTML({ kvNamespaces, r2Buckets, d1Databases }), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } async function handleKvBrowsePage(url) { const namespaceId = url.searchParams.get("ns_id"); const namespaceTitle = url.searchParams.get("title"); const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${namespaceId}/keys?limit=1000`, { headers: { "Authorization": `Bearer ${CF_API_TOKEN}` } }); const data = await response.json(); const keys = data.result || []; return new Response(generateKvBrowsePageHTML({ namespaceId, namespaceTitle, keys }), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } async function handleLogsPage(url) { const scriptName = url.searchParams.get("script"); const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const query = `query GetLogs($accountId: string, $filter: WorkersInvocationsAdaptiveFilter_InputObject) { viewer { accounts(filter: {accountTag: $accountId}) { workersInvocationsAdaptive(filter: $filter, limit: 100, orderBy: [datetime_DESC]) { datetime scriptName outcome exceptions { name message } logs { level message timestamp } } } } }`; const variables = { accountId: CF_ACCOUNT_ID, filter: { scriptName_in: [scriptName], datetime_geq: oneHourAgo.toISOString(), datetime_leq: now.toISOString() } }; const response = await fetch("https://api.cloudflare.com/client/v4/graphql", { method: "POST", headers: { "Authorization": `Bearer ${CF_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ query, variables }) }); const data = await response.json(); const logs = data.data?.viewer?.accounts[0]?.workersInvocationsAdaptive || []; return new Response(generateLogsPageHTML({ scriptName, logs }), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } // --- HTML Generation Functions --- function generateListPageHTML({ workers, domainsByWorker, zones, dailyUsage }) { const usagePercent = Math.min((dailyUsage / 100000) * 100, 100).toFixed(2); const zoneOptions = zones.map(zone => `<option value="${zone.id}">${zone.name}</option>`).join(''); // [MODIFIKASI] Menambahkan tombol Hapus Worker di sini const workerList = workers.map(worker => { const workerDomains = domainsByWorker[worker.id] || []; const detailsId = `details-${worker.id.replace(/\s+/g, '-')}`; return ` <div class="card worker-card"> <div class="card-content"> <div class="worker-header"> <h3>${worker.id}</h3> <div class="header-right"> <a href="?action=logs&script=${worker.id}" class="toggle-btn">Lihat Logs</a> <button id="btn-${detailsId}" class="toggle-btn" onclick="toggleDetails(event, '${detailsId}')">Domain</button> <a href="?action=edit&script=${worker.id}" class="action-btn">Edit</a> <form action="/api/delete-script" method="POST" onsubmit="return confirm('ANDA YAKIN INGIN MENGHAPUS WORKER INI? Tindakan ini tidak dapat dibatalkan.')" style="display: inline;"> <input type="hidden" name="scriptName" value="${worker.id}"> <button type="submit" class="delete-btn">Hapus</button> </form> </div> </div> <div id="${detailsId}" class="worker-details" style="display: none;"> <div class="domains-list">${workerDomains.map(domain => ` <div class="item"> <span>${domain.hostname}</span> <form action="/api/delete-domain" method="POST" onsubmit="return confirm('Hapus domain ${domain.hostname}?')"> <input type="hidden" name="hostname" value="${domain.hostname}"> <button type="submit" class="delete-btn">Hapus</button> </form> </div>`).join('') || '<p class="no-items">Belum ada custom domain.</p>'} </div> <div class="add-form"> <h4>Tambahkan Custom Domain</h4> <form action="/api/add-domain" method="POST"> <input type="hidden" name="scriptName" value="${worker.id}"> <select name="zoneId" required>${zoneOptions}</select> <input type="text" name="hostname" placeholder="cth: api.domainanda.com" required> <button type="submit">Tambahkan</button> </form> </div> </div> </div> </div>`; }).join(''); return `<!DOCTYPE html><html lang="id"><head><title>My Manager</title>${commonStyles()}</head> <body><div class="container"> <header><h1>Cloud Manager</h1></header> <div class="grid"> <div class="card"><div class="card-content"><h2>Total Workers</h2><div class="value">${workers.length}</div></div></div> <div class="card usage-card"><div class="card-content"><h2>Daily Requests</h2><div class="value">${dailyUsage.toLocaleString('id-ID')}</div><div class="progress-bar"><div style="width:${usagePercent}%"></div></div><div class="usage-info"><span>${usagePercent}% digunakan</span><span>Limit: 100.000</span></div></div></div> <a href="?action=create" class="card create-card"><div class="card-content"><h2>Buat Worker Baru</h2><div class="value">+</div></div></a> <a href="?action=storage" class="card create-card" style="background: linear-gradient(90deg, #3B82F6, #845EC2);"><div class="card-content"><h2>Kelola Storage</h2><div class="value">🗄️</div></div></a> </div> <h2 class="section-title">Daftar Workers</h2> <div>${workerList}</div> <footer>Create By Bang Ariv</footer> </div> <script> function toggleDetails(event, elementId) { event.stopPropagation(); const details = document.getElementById(elementId); const button = document.getElementById('btn-' + elementId); const isHidden = details.style.display === 'none'; details.style.display = isHidden ? 'block' : 'none'; button.textContent = isHidden ? 'Tutup' : 'Domain'; } </script> </body></html>`; } function generateStoragePageHTML({ kvNamespaces, r2Buckets, d1Databases }) { const renderTable = (title, headers, data, rowRenderer) => { return ` <div class="card" style="background:var(--bg-card);"> <div class="card-content"> <h2 class="section-title" style="margin-top:0;">${title}</h2> ${data.length === 0 ? '<p class="no-items">Tidak ada data.</p>' : ` <div class="table-wrapper"> <table> <thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead> <tbody>${data.map(rowRenderer).join('')}</tbody> </table> </div>`} </div> </div>`; }; return `<!DOCTYPE html><html lang="id"><head><title>Storage Manager</title>${commonStyles()} <style> .table-wrapper{overflow-x:auto;} .table-wrapper table{width:100%;border-collapse:collapse;} .table-wrapper th, .table-wrapper td{padding:12px 15px;border-bottom:1px solid var(--border-color);text-align:left;white-space:nowrap;} .table-wrapper th{font-weight:600;} .copy-btn{background-color:var(--border-color);font-size:12px;padding:4px 8px;margin-left:5px; border-radius:4px; border:none; color: var(--text-primary); cursor:pointer; text-decoration: none; display: inline-block;} .code{background-color:var(--bg-dark);padding:2px 6px;border-radius:4px;font-family:monospace;} .create-form-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px;margin-bottom:24px;} .create-form{background-color:var(--bg-card);padding:20px;border-radius:12px;} .create-form h3{margin-top:0;} .create-form input{width:100%;} .create-form button{margin-top:10px;width:100%;} </style> </head><body><div class="container"> <header><a href="/" style="text-decoration:none;color:var(--accent-blue);">← Kembali</a><h1>Storage & Databases</h1></header> <h2 class="section-title">Buat Resource Baru</h2> <div class="create-form-container"> <div class="create-form"><h3>📝 Buat KV Namespace</h3><form action="/api/create-kv" method="POST"><input type="text" name="kvTitle" placeholder="Nama KV (cth: SESSIONS)" required class="add-form-input"><button type="submit" class="action-btn">Buat KV</button></form></div> <div class="create-form"><h3>🪣 Buat R2 Bucket</h3><form action="/api/create-r2" method="POST"><input type="text" name="r2Name" placeholder="Nama Bucket (cth: my-assets)" required class="add-form-input"><button type="submit" class="action-btn">Buat R2</button></form></div> <div class="create-form"><h3>🗄️ Buat D1 Database</h3><form action="/api/create-d1" method="POST"><input type="text" name="d1Name" placeholder="Nama DB (cth: main-database)" required class="add-form-input"><button type="submit" class="action-btn">Buat D1</button></form></div> </div> ${renderTable('📝 KV Namespaces', ['Nama', 'ID', 'Aksi'], kvNamespaces, item => `<tr><td>${item.title}</td><td><code id="kv-${item.id}">${item.id}</code></td><td><a href="?action=browse-kv&ns_id=${item.id}&title=${encodeURIComponent(item.title)}" class="copy-btn">Browse</a><form action="/api/delete-kv" method="POST" onsubmit="return confirm('Yakin hapus KV Namespace ini?')" style="display:inline;"><input type="hidden" name="namespaceId" value="${item.id}"><button type="submit" class="delete-btn">Hapus</button></form></td></tr>`)} ${renderTable('🪣 R2 Buckets', ['Nama', 'Tanggal Dibuat', 'Aksi'], r2Buckets, item => `<tr><td><code>${item.name}</code></td><td>${new Date(item.creation_date).toLocaleDateString('id-ID')}</td><td><form action="/api/delete-r2" method="POST" onsubmit="return confirm('YAKIN HAPUS R2 BUCKET INI?\\nPERHATIAN: Bucket harus kosong!')"><input type="hidden" name="bucketName" value="${item.name}"><button type="submit" class="delete-btn">Hapus</button></form></td></tr>`)} ${renderTable('🗄️ D1 Databases', ['Nama', 'ID', 'Aksi'], d1Databases, item => `<tr><td>${item.name}</td><td><code>${item.uuid}</code></td><td><form action="/api/delete-d1" method="POST" onsubmit="return confirm('Yakin hapus D1 Database ini?')"><input type="hidden" name="databaseId" value="${item.uuid}"><button type="submit" class="delete-btn">Hapus</button></form></td></tr>`)} </div></body></html>`; } function generateEditorPageHTML({ pageTitle, scriptName, scriptContent, scriptBindings, availableResources }) { const templateOptions = Object.keys(WORKER_TEMPLATES).map(key => `<option value="${key}">${key}</option>`).join(''); return `<!DOCTYPE html><html lang="id"><head><title>${pageTitle}</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/theme/dracula.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/mode/javascript/javascript.min.js"></script> ${commonStyles()} <style> .editor-container,.bindings-container{padding:20px;background-color:var(--bg-card);border-radius:12px;margin-bottom:16px;} .form-group{margin-bottom:15px;} .form-group label{display:block;margin-bottom:5px;} .form-group input,.form-group select{width:100%;padding:10px;border-radius:6px;background-color:var(--border-color);border:1px solid #4b5563;color:var(--text-primary);} .actions{display:flex;justify-content:space-between;align-items:center;margin-top:20px;} .binding-row{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:center;margin-bottom:10px;padding:10px;background-color:var(--bg-dark);border-radius:8px;} .binding-row input, .binding-row select {background-color:#374151;} .CodeMirror { border: 1px solid var(--border-color); border-radius: 8px; height: 55vh; font-size: 14px; } </style> </head><body><div class="container"> <header><h1>${pageTitle}</h1></header> <form id="deploy-form" action="/api/deploy" method="POST"> <div class="editor-container"> <div class="form-group"><label for="scriptName">Nama Worker</label><input type="text" name="scriptName" value="${scriptName || ''}" ${scriptName ? 'readonly' : ''} required placeholder="nama-worker-baru"></div> ${!scriptName ? `<div class="form-group"><label for="template">Pilih Template Awal</label><select id="template-selector"><option value="">Pilih...</option>${templateOptions}</select></div>` : ''} <div class="form-group"><label for="content">Kode Sumber</label><textarea id="editor" name="content">${scriptContent.replace(/</g, "<")}</textarea></div> </div> <div class="bindings-container"> <h2 style="margin-top:0;">Bindings</h2><div id="bindings-list"></div> <div class="actions"> <button type="button" class="toggle-btn" onclick="addBinding('kv_namespace')">Tambah KV</button> <button type="button" class="toggle-btn" onclick="addBinding('r2_bucket')">Tambah R2</button> <button type="button" class="toggle-btn" onclick="addBinding('d1')">Tambah D1</button> </div> </div> <textarea name="bindings" id="bindings-json" style="display:none;"></textarea> <div class="actions"><a href="/" class="delete-btn" style="text-decoration:none;">← Kembali</a><button type="submit" class="action-btn">Simpan & Deploy</button></div> </form> ${scriptName ? `<form action="/api/delete-script" method="POST" onsubmit="return confirm('HAPUS SCRIPT INI?');"><input type="hidden" name="scriptName" value="${scriptName}"><button type="submit" class="delete-btn" style="width:100%;margin-top:15px;">Hapus Script Worker Ini</button></form>` : ''} </div> <script> var codeEditor = CodeMirror.fromTextArea(document.getElementById("editor"), { lineNumbers: true, mode: "javascript", theme: "dracula", lineWrapping: true }); const templates = ${JSON.stringify(WORKER_TEMPLATES)}; const templateSelector = document.getElementById('template-selector'); if(templateSelector) { templateSelector.addEventListener('change', (e) => { if(e.target.value) codeEditor.setValue(templates[e.target.value]); }); } const availableResources = ${JSON.stringify(availableResources)}; const currentBindings = ${JSON.stringify(scriptBindings)}; const bindingsList = document.getElementById('bindings-list'); const resourceMap = { kv_namespace: { items: availableResources.kv, key: 'title', value: 'id', label: 'KV Namespace' }, r2_bucket: { items: availableResources.r2, key: 'name', value: 'name', label: 'R2 Bucket' }, d1: { items: availableResources.d1, key: 'name', value: 'uuid', label: 'D1 Database' } }; function renderBinding(binding) { const typeInfo = resourceMap[binding.type]; if (!typeInfo) return; const row = document.createElement('div'); row.className = 'binding-row'; row.dataset.type = binding.type; let resourceId = ''; if (binding.type === 'kv_namespace') resourceId = binding.namespace_id; if (binding.type === 'r2_bucket') resourceId = binding.bucket_name; if (binding.type === 'd1') resourceId = binding.database_id; row.innerHTML = \`<input type="text" placeholder="Nama Variabel (cth: MY_KV)" value="\${binding.name || ''}" class="binding-name"><select class="binding-resource"><option value="">Pilih \${typeInfo.label}...</option>\${typeInfo.items.map(item => \`<option value="\${item[typeInfo.value]}" \${item[typeInfo.value] === resourceId ? 'selected' : ''}>\${item[typeInfo.key]}</option>\`).join('')}</select><button type="button" class="delete-btn" onclick="this.parentElement.remove()">Hapus</button>\`; bindingsList.appendChild(row); } function addBinding(type) { renderBinding({ type: type }); } currentBindings.forEach(renderBinding); document.getElementById('deploy-form').addEventListener('submit', (e) => { codeEditor.save(); const bindings = []; document.querySelectorAll('.binding-row').forEach(row => { const type = row.dataset.type; const name = row.querySelector('.binding-name').value; const resource = row.querySelector('.binding-resource').value; if (!name || !resource) return; let binding = { type, name }; if (type === 'kv_namespace') binding.namespace_id = resource; if (type === 'r2_bucket') binding.bucket_name = resource; if (type === 'd1') binding.database_id = resource; bindings.push(binding); }); document.getElementById('bindings-json').value = JSON.stringify(bindings, null, 2); }); </script> </body></html>`; } function generateKvBrowsePageHTML({ namespaceId, namespaceTitle, keys }) { return `<!DOCTYPE html><html lang="id"><head><title>KV: ${namespaceTitle}</title>${commonStyles()} <style> .kv-container{padding:20px;background-color:var(--bg-card);border-radius:12px;margin-bottom:16px;} .kv-form{display:grid; grid-template-columns:1fr; gap:10px;} .kv-form textarea{width:100%;height:15vh;background-color:#0d1117;color:#c9d1d9;border:1px solid var(--border-color);border-radius:8px;font-family:monospace;padding:10px;} .kv-table{width:100%;margin-top:20px;border-collapse:collapse;} .kv-table th, .kv-table td{padding:10px;border-bottom:1px solid var(--border-color);text-align:left;} .kv-table code{background-color:var(--bg-dark);padding:2px 6px;border-radius:4px;font-family:monospace;} </style> </head><body><div class="container"> <header><a href="?action=storage" style="text-decoration:none;color:var(--accent-blue);">← Kembali</a><h1>Penjelajah KV: ${namespaceTitle}</h1></header> <div class="kv-container"><h2 style="margin-top:0;">Tambah / Edit Key</h2> <form id="kv-put-form" action="/api/kv-put" method="POST"> <input type="hidden" name="namespaceId" value="${namespaceId}"><input type="hidden" name="title" value="${namespaceTitle}"> <div class="kv-form"> <input type="text" name="key" id="key-input" placeholder="Nama Key" required class="add-form-input"> <textarea name="value" id="value-input" placeholder="Value (string)"></textarea> <button type="submit" class="action-btn">Simpan</button> </div> </form> </div> <div class="kv-container"><h2 style="margin-top:0;">Daftar Keys</h2> <table class="kv-table"> <thead><tr><th>Key</th><th>Aksi</th></tr></thead> <tbody> ${keys.map(key => ` <tr><td><code>${key.name}</code></td> <td> <button class="toggle-btn" onclick="viewOrEditKey('${namespaceId}', '${key.name}')">Lihat/Edit</button> <form action="/api/kv-delete-key" method="POST" onsubmit="return confirm('Yakin hapus key ini?')" style="display:inline;"> <input type="hidden" name="namespaceId" value="${namespaceId}"><input type="hidden" name="key" value="${key.name}"><input type="hidden" name="title" value="${namespaceTitle}"> <button type="submit" class="delete-btn">Hapus</button> </form> </td></tr>`).join('') || `<tr><td colspan="2" style="text-align:center;">Tidak ada key.</td></tr>`} </tbody> </table> </div> </div> <script> async function viewOrEditKey(ns_id, key) { document.getElementById('key-input').value = key; document.getElementById('value-input').value = 'Memuat...'; const response = await fetch(\`?action=get-kv-value&ns_id=\${ns_id}&key=\${encodeURIComponent(key)}\`); if(response.ok) { document.getElementById('value-input').value = await response.text(); window.scrollTo(0, 0); } else { document.getElementById('value-input').value = 'Gagal memuat value.'; } } </script> </body></html>`; } function generateLogsPageHTML({ scriptName, logs }) { return `<!DOCTYPE html><html lang="id"><head><title>Logs: ${scriptName}</title>${commonStyles()} <style> .log-entry { background-color: var(--bg-card); border-radius: 8px; padding: 15px; margin-bottom: 10px; font-family: monospace; font-size: 14px; } .log-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 8px; margin-bottom: 8px; flex-wrap: wrap; gap: 10px; } .log-outcome-Ok { color: #22c55e; font-weight: bold; } .log-outcome-Exception { color: var(--accent-red); font-weight: bold; } .log-level-log { color: var(--text-secondary); } .log-level-error { color: #f97316; } .log-level-warn { color: #eab308; } .log-message { white-space: pre-wrap; word-wrap: break-word; } .exception-block { background-color: rgba(239, 68, 68, 0.1); border-left: 3px solid var(--accent-red); padding: 10px; margin-top: 10px; border-radius: 4px; } </style> </head><body><div class="container"> <header><a href="/" style="text-decoration:none;color:var(--accent-blue);">← Kembali</a><h1>Logs untuk: ${scriptName}</h1><p class="no-items">Menampilkan log dalam satu jam terakhir.</p></header> <div id="logs-container"> ${logs.length === 0 ? '<div class="log-entry"><p class="no-items" style="text-align:center;">Tidak ada log ditemukan.</p></div>' : logs.map(log => ` <div class="log-entry"> <div class="log-header"> <span>${new Date(log.datetime).toLocaleString('id-ID', { timeZone: 'Asia/Jakarta', dateStyle:'short', timeStyle:'medium' })}</span> <span class="log-outcome-${log.outcome}">${log.outcome}</span> </div> <div class="log-body"> ${log.logs.map(line => `<div class="log-level-${line.level}"><span style="opacity:0.5;">[${line.level.toUpperCase()}]</span> <span class="log-message">${line.message.map(m=>typeof m === 'object' ? JSON.stringify(m) : m).join(' ')}</span></div>`).join('')} ${log.exceptions.map(ex => `<div class="exception-block"><b>${ex.name}:</b> ${ex.message}</div>`).join('')} </div> </div>`).join('')} </div> </div></body></html>`; } function commonStyles() { return `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <style> :root{--bg-dark:#111827;--bg-card:#1f2d37;--border-color:#374151;--text-primary:#f9fafb;--text-secondary:#9ca3af;--accent-blue:#3b82f6;--accent-red:#ef4444;} *{box-sizing:border-box;} body{font-family:'Inter',sans-serif;background-color:var(--bg-dark);margin:0;padding:16px;color:var(--text-primary);} .container{max-width:900px;margin:auto;} header h1{font-size:24px;text-align:center;margin-bottom:24px;} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px;} .card{padding:2px;border-radius:14px;background:linear-gradient(90deg,#845EC2,#FF6F91,#FFC75F,#845EC2);background-size:400%;animation:animated-gradient 8s ease-in-out infinite;} .card-content{background-color:var(--bg-card);padding:20px;border-radius:12px;width:100%;height:100%;} @keyframes animated-gradient{0%,100%{background-position:0% 50%;}50%{background-position:100% 50%;}} .create-card{text-decoration:none;color:var(--text-primary);text-align:center;} .create-card .value{font-size:48px;font-weight:300;color:var(--text-secondary);} .card h2{font-size:14px;color:var(--text-secondary);margin:0 0 8px 0;} .card .value{font-size:32px;font-weight:700;} .progress-bar{width:100%;background-color:var(--border-color);border-radius:8px;height:12px;overflow:hidden;margin-top:8px;} .progress-bar div{height:100%;background-color:#2563eb;border-radius:8px;} .usage-info{display:flex;justify-content:space-between;font-size:14px;margin-top:8px;} .section-title{font-size:18px;font-weight:600;margin:32px 0 16px 0;border-bottom:1px solid var(--border-color);padding-bottom:10px;} .worker-card{margin-bottom:16px;} .worker-header{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;padding:4px;} .worker-header h3{margin:0;font-size:16px;word-break:break-all;flex-basis:100%;} .header-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;} .toggle-btn,.action-btn{background-color:var(--accent-blue);color:var(--text-primary);border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-weight:500;font-size:14px;text-decoration:none;white-space:nowrap;} .toggle-btn { background-color: var(--border-color); } .worker-details{display:none;margin-top:16px;border-top:1px solid var(--border-color);padding-top:16px;} .item{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-color);flex-wrap:wrap;gap:10px;} .item:last-child{border-bottom:none;} .item span{word-break:break-all;} .no-items{color:var(--text-secondary);font-size:14px;} .delete-btn{background-color:transparent;color:var(--accent-red);border:1px solid var(--accent-red);padding:6px 12px;border-radius:6px;cursor:pointer;font-weight:500;margin-left:auto;} .add-form{margin-top:20px;border-top:1px solid var(--border-color);padding-top:20px;} .add-form form{display:flex;flex-direction:column;gap:10px;} .add-form input, .add-form select, .add-form-input {width:100%;background-color:var(--border-color);color:var(--text-primary);border:1px solid #4b5563;padding:10px;border-radius:6px;} footer{text-align:center;margin-top:40px;color:var(--text-secondary);font-size:14px;} @media(min-width:640px){.worker-header{flex-wrap:nowrap;}.worker-header h3{flex-basis:auto;}.add-form form{flex-direction:row;}} </style>`; } --264a4f5f1706602b218129b0f3f3ba55e1a7abe45544adf3bbdee984c8e9--
Bindings
Tambah KV
Tambah R2
Tambah D1
← Kembali
Simpan & Deploy
Hapus Script Worker Ini