'use strict';
'require view';
'require rpc';
'require ui';
'require fs';
var callDropletStatus = rpc.declare({
object: 'luci.droplet',
method: 'status',
expect: {}
});
var callDropletList = rpc.declare({
object: 'luci.droplet',
method: 'list',
expect: { droplets: [] }
});
var callDropletUpload = rpc.declare({
object: 'luci.droplet',
method: 'upload',
params: ['file', 'name', 'domain'],
expect: {}
});
var callDropletRemove = rpc.declare({
object: 'luci.droplet',
method: 'remove',
params: ['name'],
expect: {}
});
var callDropletJobStatus = rpc.declare({
object: 'luci.droplet',
method: 'job_status',
params: ['job_id'],
expect: {}
});
return view.extend({
load: function() {
return Promise.all([
callDropletStatus(),
callDropletList()
]);
},
render: function(data) {
var status = data[0] || {};
var droplets = data[1] || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('style', {}, `
.droplet-container { max-width: 800px; margin: 0 auto; }
.drop-zone {
border: 3px dashed #00d4ff;
border-radius: 16px;
padding: 60px 40px;
text-align: center;
background: linear-gradient(135deg, rgba(0,212,255,0.05), rgba(124,58,237,0.05));
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 20px;
}
.drop-zone:hover, .drop-zone.drag-over {
border-color: #7c3aed;
background: linear-gradient(135deg, rgba(0,212,255,0.1), rgba(124,58,237,0.1));
transform: scale(1.02);
}
.drop-zone h2 { color: #00d4ff; margin: 0 0 10px; font-size: 1.5em; }
.drop-zone p { color: #888; margin: 0; }
.drop-zone input[type="file"] { display: none; }
.publish-form {
display: none;
background: #1a1a24;
border: 1px solid #2a2a3e;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.publish-form.active { display: block; }
.publish-form .field { margin-bottom: 15px; }
.publish-form label { display: block; color: #888; margin-bottom: 5px; font-size: 0.9em; }
.publish-form input[type="text"] {
width: 100%;
padding: 10px 15px;
background: #12121a;
border: 1px solid #2a2a3e;
border-radius: 8px;
color: #e0e0e0;
font-size: 1em;
}
.publish-form input:focus { outline: none; border-color: #00d4ff; }
.publish-form .file-info {
background: #12121a;
padding: 10px 15px;
border-radius: 8px;
color: #00d4ff;
font-family: monospace;
margin-bottom: 15px;
}
.publish-form .buttons { display: flex; gap: 10px; }
.btn-publish {
flex: 1;
padding: 12px 24px;
background: linear-gradient(135deg, #00d4ff, #7c3aed);
border: none;
border-radius: 8px;
color: #fff;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.btn-publish:hover { transform: translateY(-2px); }
.btn-cancel {
padding: 12px 24px;
background: #2a2a3e;
border: none;
border-radius: 8px;
color: #888;
cursor: pointer;
}
.droplet-list { margin-top: 30px; }
.droplet-list h3 { color: #e0e0e0; margin-bottom: 15px; }
.droplet-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
background: #1a1a24;
border: 1px solid #2a2a3e;
border-radius: 8px;
margin-bottom: 10px;
}
.droplet-item:hover { border-color: #00d4ff; }
.droplet-info { flex: 1; }
.droplet-name { font-weight: 600; color: #e0e0e0; }
.droplet-domain { font-size: 0.85em; color: #00d4ff; font-family: monospace; }
.droplet-type {
font-size: 0.75em;
padding: 2px 8px;
background: rgba(124,58,237,0.2);
color: #7c3aed;
border-radius: 4px;
margin-left: 10px;
}
.droplet-actions button {
padding: 6px 12px;
background: #2a2a3e;
border: none;
border-radius: 4px;
color: #888;
cursor: pointer;
margin-left: 5px;
}
.droplet-actions button:hover { background: #3a3a4e; color: #e0e0e0; }
.droplet-actions .btn-delete:hover { background: #ef4444; color: #fff; }
.result-message {
display: none;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.result-message.success { display: block; background: rgba(16,185,129,0.1); border: 1px solid #10b981; color: #10b981; }
.result-message.error { display: block; background: rgba(239,68,68,0.1); border: 1px solid #ef4444; color: #ef4444; }
.result-message a { color: inherit; }
`),
E('div', { 'class': 'droplet-container' }, [
E('h2', { 'style': 'color: #e0e0e0; margin-bottom: 20px;' }, [
E('span', { 'style': 'color: #00d4ff;' }, 'Droplet'),
' Publisher'
]),
E('div', { 'class': 'result-message', 'id': 'result-msg' }),
E('div', { 'class': 'drop-zone', 'id': 'drop-zone' }, [
E('h2', {}, '📦 Drop to Publish'),
E('p', {}, 'Drop HTML files or ZIP archives here'),
E('p', { 'style': 'margin-top: 10px; font-size: 0.85em;' }, 'or click to browse (multiple files supported)'),
E('input', { 'type': 'file', 'id': 'file-input', 'accept': '.html,.htm,.zip', 'multiple': true })
]),
E('div', { 'class': 'publish-form', 'id': 'publish-form' }, [
E('div', { 'id': 'files-list', 'style': 'margin-bottom: 15px;' }),
E('div', { 'class': 'field' }, [
E('label', {}, 'Domain'),
E('input', { 'type': 'text', 'id': 'site-domain', 'value': status.default_domain || 'gk2.secubox.in', 'placeholder': 'gk2.secubox.in' })
]),
E('div', { 'class': 'buttons' }, [
E('button', { 'class': 'btn-cancel', 'id': 'btn-cancel' }, 'Cancel'),
E('button', { 'class': 'btn-publish', 'id': 'btn-publish' }, '🚀 Publish')
])
]),
E('div', { 'class': 'droplet-list' }, [
E('h3', {}, 'Published Droplets (' + droplets.length + ')'),
E('div', { 'id': 'droplet-items' },
droplets.map(function(d) {
return E('div', { 'class': 'droplet-item', 'data-name': d.name }, [
E('div', { 'class': 'droplet-info' }, [
E('span', { 'class': 'droplet-name' }, d.name),
E('span', { 'class': 'droplet-type' }, d.type || 'static'),
E('div', { 'class': 'droplet-domain' }, [
E('a', { 'href': 'https://' + d.domain + '/', 'target': '_blank' }, d.domain)
])
]),
E('div', { 'class': 'droplet-actions' }, [
E('button', { 'class': 'btn-open', 'data-url': 'https://' + d.domain + '/' }, '🔗'),
E('button', { 'class': 'btn-delete', 'data-name': d.name }, '🗑')
])
]);
})
)
])
])
]);
// Event handlers
var dropZone = view.querySelector('#drop-zone');
var fileInput = view.querySelector('#file-input');
var publishForm = view.querySelector('#publish-form');
var filesList = view.querySelector('#files-list');
var siteDomain = view.querySelector('#site-domain');
var btnPublish = view.querySelector('#btn-publish');
var btnCancel = view.querySelector('#btn-cancel');
var resultMsg = view.querySelector('#result-msg');
var selectedFiles = [];
// Drag & drop
dropZone.addEventListener('click', function() { fileInput.click(); });
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', function() {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('drag-over');
if (e.dataTransfer.files.length) {
handleFiles(Array.from(e.dataTransfer.files));
}
});
fileInput.addEventListener('change', function() {
if (fileInput.files.length) {
handleFiles(Array.from(fileInput.files));
}
});
function handleFiles(files) {
selectedFiles = files.map(function(file) {
var name = file.name.replace(/\.(html?|zip)$/i, '').toLowerCase().replace(/[^a-z0-9_-]/g, '_');
return { file: file, name: name };
});
// Build files list UI
filesList.innerHTML = '';
selectedFiles.forEach(function(item, idx) {
var row = E('div', { 'class': 'file-info', 'style': 'display: flex; align-items: center; gap: 10px; margin-bottom: 8px;' }, [
E('span', { 'style': 'flex: 0 0 auto;' }, '📄'),
E('span', { 'style': 'flex: 1; overflow: hidden; text-overflow: ellipsis;' }, item.file.name + ' (' + formatSize(item.file.size) + ')'),
E('input', {
'type': 'text',
'value': item.name,
'placeholder': 'site name',
'data-idx': idx,
'class': 'file-name-input',
'style': 'width: 150px; padding: 5px 10px; background: #12121a; border: 1px solid #2a2a3e; border-radius: 4px; color: #e0e0e0;'
})
]);
filesList.appendChild(row);
});
// Update names on input change
filesList.querySelectorAll('.file-name-input').forEach(function(input) {
input.addEventListener('input', function() {
selectedFiles[parseInt(input.dataset.idx)].name = input.value;
});
});
publishForm.classList.add('active');
dropZone.style.display = 'none';
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
btnCancel.addEventListener('click', function() {
publishForm.classList.remove('active');
dropZone.style.display = 'block';
selectedFiles = [];
fileInput.value = '';
filesList.innerHTML = '';
});
btnPublish.addEventListener('click', function() {
// Validate all files have names
var valid = selectedFiles.every(function(item) { return item.name && item.name.trim(); });
if (!selectedFiles.length || !valid) {
showResult('error', 'Please select files and enter names for all');
return;
}
btnPublish.disabled = true;
var total = selectedFiles.length;
var completed = 0;
var errors = [];
showResult('success', '⏳ Publishing ' + total + ' file(s)...');
// Process files sequentially
function processNext(idx) {
if (idx >= selectedFiles.length) {
// All done
btnPublish.disabled = false;
btnPublish.textContent = '🚀 Publish';
if (errors.length) {
showResult('error', '❌ ' + errors.join('
'));
} else {
showResult('success', '✅ Published ' + total + ' droplet(s)!');
setTimeout(function() { location.reload(); }, 2000);
}
return;
}
var item = selectedFiles[idx];
btnPublish.textContent = '⏳ ' + (idx + 1) + '/' + total + '...';
// Upload file
var formData = new FormData();
formData.append('sessionid', rpc.getSessionID());
formData.append('filename', '/tmp/droplet-upload/' + item.file.name);
formData.append('filedata', item.file);
fetch('/cgi-bin/cgi-upload', {
method: 'POST',
body: formData
})
.then(function(res) { return res.json(); })
.then(function(uploadRes) {
if (uploadRes.size) {
return callDropletUpload(item.file.name, item.name, siteDomain.value);
} else {
throw new Error('Upload failed for ' + item.file.name);
}
})
.then(function(result) {
if (result.status === 'started' && result.job_id) {
return pollJobStatus(result.job_id, false);
} else if (!result.success && result.error) {
throw new Error(item.name + ': ' + result.error);
}
})
.then(function() {
completed++;
processNext(idx + 1);
})
.catch(function(err) {
errors.push(item.name + ': ' + err.message);
processNext(idx + 1);
});
}
processNext(0);
});
function showResult(type, msg) {
resultMsg.className = 'result-message ' + type;
resultMsg.innerHTML = msg;
}
function pollJobStatus(jobId, showMessages) {
if (showMessages === undefined) showMessages = true;
return new Promise(function(resolve, reject) {
var attempts = 0;
var maxAttempts = 60; // 60 * 2s = 2 minutes max
function check() {
callDropletJobStatus(jobId).then(function(status) {
if (status.status === 'complete') {
if (status.success) {
if (showMessages) {
showResult('success', '✅ Published! ' + status.url + '');
setTimeout(function() { location.reload(); }, 2000);
}
resolve(status);
} else {
if (showMessages) {
showResult('error', '❌ ' + (status.error || 'Failed to publish'));
}
reject(new Error(status.error || 'Failed to publish'));
}
} else if (status.status === 'running') {
attempts++;
if (attempts < maxAttempts) {
setTimeout(check, 2000);
} else {
if (showMessages) showResult('error', '❌ Publish timed out');
reject(new Error('Timeout'));
}
} else {
if (showMessages) showResult('error', '❌ Job not found');
reject(new Error('Job not found'));
}
}).catch(function(err) {
attempts++;
if (attempts < maxAttempts) {
setTimeout(check, 2000);
} else {
reject(err);
}
});
}
check();
});
}
// Delete buttons
view.querySelectorAll('.btn-delete').forEach(function(btn) {
btn.addEventListener('click', function() {
var name = btn.dataset.name;
if (confirm('Delete "' + name + '"?')) {
callDropletRemove(name).then(function() {
btn.closest('.droplet-item').remove();
showResult('success', 'Deleted: ' + name);
});
}
});
});
// Open buttons
view.querySelectorAll('.btn-open').forEach(function(btn) {
btn.addEventListener('click', function() {
window.open(btn.dataset.url, '_blank');
});
});
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});