secubox-openwrt/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/dashboard.js
CyberMind-FR 163364843e feat(metablogizer): KISS UI redesign with backend status
- Replace overview.js with dashboard.js using standard cbi-* classes
- Add api.js module for RPC declarations
- Show port, runtime, backend_running status in sites table
- Add sync_config, discover_vhosts, import_vhost RPC methods
- Update ACL with new method permissions
- Menu: Sites -> Dashboard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 08:06:22 +01:00

696 lines
25 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require fs';
'require metablogizer.api as api';
'require metablogizer/qrcode as qrcode';
return view.extend({
status: {},
sites: [],
hosting: {},
uploadFiles: [],
currentSite: null,
load: function() {
var self = this;
return api.getDashboardData().then(function(data) {
self.status = data.status || {};
self.sites = data.sites || [];
self.hosting = data.hosting || {};
});
},
render: function() {
var self = this;
var status = this.status;
var sites = this.sites;
var hosting = this.hosting;
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('MetaBlogizer')),
E('div', { 'class': 'cbi-map-descr' }, _('Static site publisher with HAProxy vhosts and SSL')),
// Status Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Status')),
E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'style': 'width:200px' }, _('Runtime')),
E('td', { 'class': 'td' }, status.detected_runtime || 'uhttpd')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('HAProxy')),
E('td', { 'class': 'td' }, hosting.haproxy_status === 'running' ?
E('span', { 'style': 'color:#0a0' }, _('Running')) :
E('span', { 'style': 'color:#a00' }, _('Stopped')))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Public IP')),
E('td', { 'class': 'td' }, hosting.public_ip || '-')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Sites')),
E('td', { 'class': 'td' }, String(sites.length))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Backends Running')),
E('td', { 'class': 'td' }, String(sites.filter(function(s) { return s.backend_running; }).length) + ' / ' + sites.length)
])
]),
E('div', { 'style': 'margin-top:1em' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, 'handleSyncConfig')
}, _('Sync Config')),
' ',
E('span', { 'class': 'cbi-value-description' }, _('Update port/runtime info for all sites'))
])
]),
// Sites Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Sites')),
sites.length > 0 ? this.renderSitesTable(sites) : E('div', { 'class': 'cbi-section-descr' }, _('No sites configured'))
]),
// Create Site Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Create Site')),
E('div', { 'class': 'cbi-section-descr' }, _('Add a new static site with auto-configured HAProxy vhost and SSL')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Site Name')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'text', 'id': 'new-site-name', 'class': 'cbi-input-text',
'placeholder': 'myblog' }),
E('div', { 'class': 'cbi-value-description' }, _('Lowercase letters, numbers, and hyphens only'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'text', 'id': 'new-site-domain', 'class': 'cbi-input-text',
'placeholder': 'blog.example.com' }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Gitea Repository')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'text', 'id': 'new-site-gitea', 'class': 'cbi-input-text',
'placeholder': 'user/repo' }),
E('div', { 'class': 'cbi-value-description' }, _('Optional: Sync content from Gitea'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Description')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'text', 'id': 'new-site-desc', 'class': 'cbi-input-text',
'placeholder': 'Short description (optional)' }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('HTTPS')),
E('div', { 'class': 'cbi-value-field' },
E('select', { 'id': 'new-site-ssl', 'class': 'cbi-input-select' }, [
E('option', { 'value': '1', 'selected': true }, _('Enabled (ACME)')),
E('option', { 'value': '0' }, _('Disabled'))
]))
]),
E('div', { 'class': 'cbi-page-actions' },
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleCreateSite')
}, _('Create Site')))
]),
// Hosting Status Section
this.renderHostingSection(hosting)
]);
},
renderSitesTable: function(sites) {
var self = this;
return E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Name')),
E('th', { 'class': 'th' }, _('Domain')),
E('th', { 'class': 'th' }, _('Port')),
E('th', { 'class': 'th' }, _('Backend')),
E('th', { 'class': 'th' }, _('Content')),
E('th', { 'class': 'th' }, _('Actions'))
])
].concat(sites.map(function(site) {
var backendStatus = site.backend_running ?
E('span', { 'style': 'color:#0a0' }, _('Running')) :
E('span', { 'style': 'color:#a00' }, _('Stopped'));
var contentStatus = site.has_content ?
E('span', { 'style': 'color:#0a0' }, _('OK')) :
E('span', { 'style': 'color:#888' }, _('Empty'));
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, [
E('strong', {}, site.name),
site.runtime ? E('br') : '',
site.runtime ? E('small', { 'style': 'color:#888' }, site.runtime) : ''
]),
E('td', { 'class': 'td' }, site.domain ?
E('a', { 'href': site.url || ('https://' + site.domain), 'target': '_blank' }, site.domain) :
E('em', {}, '-')),
E('td', { 'class': 'td' }, site.port ? String(site.port) : '-'),
E('td', { 'class': 'td' }, backendStatus),
E('td', { 'class': 'td' }, contentStatus),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(self, 'showShareModal', site),
'title': _('Share')
}, _('Share')),
' ',
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(self, 'showUploadModal', site),
'title': _('Upload')
}, _('Upload')),
' ',
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(self, 'showFilesModal', site),
'title': _('Files')
}, _('Files')),
' ',
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(self, 'showEditModal', site),
'title': _('Edit')
}, _('Edit')),
' ',
site.gitea_repo ? E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(self, 'handleSync', site),
'title': _('Sync')
}, _('Sync')) : '',
' ',
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': ui.createHandlerFn(self, 'handleDelete', site),
'title': _('Delete')
}, _('Delete'))
])
]);
})));
},
renderHostingSection: function(hosting) {
var hostingSites = hosting.sites || [];
if (hostingSites.length === 0) return E('div');
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Hosting Status')),
E('div', { 'class': 'cbi-section-descr' }, _('DNS, SSL certificates, and publish status for each site')),
E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Site')),
E('th', { 'class': 'th' }, _('DNS')),
E('th', { 'class': 'th' }, _('IP')),
E('th', { 'class': 'th' }, _('Certificate')),
E('th', { 'class': 'th' }, _('Status'))
])
].concat(hostingSites.map(function(site) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, E('strong', {}, site.name)),
E('td', { 'class': 'td' }, site.dns_status === 'ok' ?
E('span', { 'style': 'color:#0a0' }, 'OK') :
E('span', { 'style': 'color:#a00' }, site.dns_status || 'unknown')),
E('td', { 'class': 'td' }, site.dns_ip || '-'),
E('td', { 'class': 'td' }, site.cert_status === 'ok' ?
E('span', { 'style': 'color:#0a0' }, (site.cert_days || 0) + 'd') :
E('span', { 'style': 'color:#a00' }, site.cert_status || 'missing')),
E('td', { 'class': 'td' }, site.publish_status === 'published' ?
E('span', { 'style': 'color:#0a0' }, _('Published')) :
E('span', { 'style': 'color:#888' }, site.publish_status || 'pending'))
]);
})))
]);
},
handleCreateSite: function() {
var self = this;
var name = document.getElementById('new-site-name').value.trim();
var domain = document.getElementById('new-site-domain').value.trim();
var gitea = document.getElementById('new-site-gitea').value.trim();
var desc = document.getElementById('new-site-desc').value.trim();
var ssl = document.getElementById('new-site-ssl').value;
if (!name) {
ui.addNotification(null, E('p', _('Site name is required')), 'error');
return;
}
if (!domain) {
ui.addNotification(null, E('p', _('Domain is required')), 'error');
return;
}
if (!/^[a-z0-9-]+$/.test(name)) {
ui.addNotification(null, E('p', _('Invalid name format: use lowercase letters, numbers, and hyphens')), 'error');
return;
}
ui.showModal(_('Creating Site'), [
E('p', { 'class': 'spinning' }, _('Setting up site and HAProxy vhost...'))
]);
api.createSite(name, domain, gitea, ssl, desc).then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', _('Site created successfully')));
self.showShareModal({ name: r.name || name, domain: r.domain || domain, url: r.url });
setTimeout(function() { window.location.reload(); }, 500);
} else {
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
showEditModal: function(site) {
var self = this;
ui.showModal(_('Edit Site: ') + site.name, [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'text', 'id': 'edit-site-domain', 'class': 'cbi-input-text',
'value': site.domain || '' }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Gitea Repository')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'text', 'id': 'edit-site-gitea', 'class': 'cbi-input-text',
'value': site.gitea_repo || '', 'placeholder': 'user/repo' }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Description')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'text', 'id': 'edit-site-desc', 'class': 'cbi-input-text',
'value': site.description || '' }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('HTTPS')),
E('div', { 'class': 'cbi-value-field' },
E('select', { 'id': 'edit-site-ssl', 'class': 'cbi-input-select' }, [
E('option', { 'value': '1', 'selected': site.ssl !== false }, _('Enabled')),
E('option', { 'value': '0', 'selected': site.ssl === false }, _('Disabled'))
]))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Enabled')),
E('div', { 'class': 'cbi-value-field' },
E('select', { 'id': 'edit-site-enabled', 'class': 'cbi-input-select' }, [
E('option', { 'value': '1', 'selected': site.enabled !== false }, _('Yes')),
E('option', { 'value': '0', 'selected': site.enabled === false }, _('No'))
]))
])
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
' ',
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
var domain = document.getElementById('edit-site-domain').value.trim();
var gitea = document.getElementById('edit-site-gitea').value.trim();
var desc = document.getElementById('edit-site-desc').value.trim();
var ssl = document.getElementById('edit-site-ssl').value;
var enabled = document.getElementById('edit-site-enabled').value;
if (!domain) {
ui.addNotification(null, E('p', _('Domain required')), 'error');
return;
}
ui.hideModal();
ui.showModal(_('Saving'), [E('p', { 'class': 'spinning' }, _('Updating site...'))]);
api.updateSite(site.id, site.name, domain, gitea, ssl, enabled, desc).then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', _('Site updated')));
window.location.reload();
} else {
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
}}, _('Save'))
])
]);
},
showUploadModal: function(site) {
var self = this;
this.uploadFiles = [];
this.currentSite = site;
var fileList = E('div', { 'id': 'upload-file-list', 'style': 'margin:1em 0; max-height:200px; overflow-y:auto' });
ui.showModal(_('Upload to: ') + site.name, [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Files')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'file', 'id': 'upload-file-input', 'multiple': true,
'change': function(e) { self.handleFileSelect(e, fileList); } }),
E('div', { 'class': 'cbi-value-description' }, _('Select HTML, CSS, JS, images, etc.'))
])
]),
fileList,
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Set first HTML as index')),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'type': 'checkbox', 'id': 'upload-as-index', 'checked': true }))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field cbi-value-description' },
_('After upload, use Ctrl+Shift+R to refresh cached pages'))
])
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
' ',
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, 'handleUpload') }, _('Upload'))
])
]);
},
handleFileSelect: function(e, listEl) {
var files = e.target.files;
for (var i = 0; i < files.length; i++) {
this.uploadFiles.push(files[i]);
}
this.updateFileList(listEl);
},
updateFileList: function(listEl) {
var self = this;
listEl.innerHTML = '';
if (this.uploadFiles.length === 0) return;
this.uploadFiles.forEach(function(f, i) {
var row = E('div', { 'style': 'display:flex; align-items:center; gap:0.5em; margin:0.25em 0; padding:0.25em; background:#f8f8f8; border-radius:4px' }, [
E('span', { 'style': 'flex:1' }, f.name),
E('span', { 'style': 'color:#888; font-size:0.9em' }, self.formatSize(f.size)),
E('button', { 'class': 'cbi-button cbi-button-remove', 'style': 'padding:0.25em 0.5em',
'click': function() { self.uploadFiles.splice(i, 1); self.updateFileList(listEl); } }, 'X')
]);
listEl.appendChild(row);
});
},
handleUpload: function() {
var self = this;
if (!this.uploadFiles.length) {
ui.addNotification(null, E('p', _('No files selected')), 'error');
return;
}
var site = this.currentSite;
var asIndex = document.getElementById('upload-as-index').checked;
var firstHtml = null;
if (asIndex) {
for (var i = 0; i < this.uploadFiles.length; i++) {
if (this.uploadFiles[i].name.endsWith('.html')) {
firstHtml = this.uploadFiles[i];
break;
}
}
}
ui.hideModal();
ui.showModal(_('Uploading'), [E('p', { 'class': 'spinning' }, _('Uploading files...'))]);
var sitesRoot = '/srv/metablogizer/sites';
Promise.all(this.uploadFiles.map(function(f) {
return new Promise(function(resolve) {
var reader = new FileReader();
reader.onload = function(e) {
var dest = (asIndex && f === firstHtml) ? 'index.html' : f.name;
fs.write(sitesRoot + '/' + site.name + '/' + dest, e.target.result)
.then(function() { resolve({ ok: true, name: f.name }); })
.catch(function() { resolve({ ok: false, name: f.name }); });
};
reader.onerror = function() { resolve({ ok: false, name: f.name }); };
reader.readAsText(f);
});
})).then(function(results) {
ui.hideModal();
var ok = results.filter(function(r) { return r.ok; }).length;
ui.addNotification(null, E('p', ok + _(' file(s) uploaded successfully')));
self.uploadFiles = [];
});
},
showFilesModal: function(site) {
var self = this;
this.currentSite = site;
var sitesRoot = '/srv/metablogizer/sites';
ui.showModal(_('Files: ') + site.name, [
E('div', { 'id': 'files-list' }, [
E('p', { 'class': 'spinning' }, _('Loading files...'))
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close'))
])
]);
fs.list(sitesRoot + '/' + site.name).then(function(files) {
var container = document.getElementById('files-list');
container.innerHTML = '';
if (!files || !files.length) {
container.appendChild(E('p', { 'style': 'color:#888' }, _('No files')));
return;
}
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('File')),
E('th', { 'class': 'th' }, _('Size')),
E('th', { 'class': 'th' }, _('Actions'))
])
]);
files.forEach(function(f) {
if (f.type !== 'file') return;
var isIndex = f.name === 'index.html';
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, [
isIndex ? E('strong', {}, f.name + ' (homepage)') : f.name
]),
E('td', { 'class': 'td' }, self.formatSize(f.size)),
E('td', { 'class': 'td' }, [
(!isIndex && f.name.endsWith('.html')) ? E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() { self.setAsHomepage(site, f.name); }
}, _('Set as Homepage')) : '',
' ',
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.deleteFile(site, f.name); }
}, _('Delete'))
])
]));
});
container.appendChild(table);
}).catch(function(e) {
var container = document.getElementById('files-list');
container.innerHTML = '';
container.appendChild(E('p', { 'style': 'color:#a00' }, _('Error: ') + e.message));
});
},
setAsHomepage: function(site, filename) {
var sitesRoot = '/srv/metablogizer/sites';
var path = sitesRoot + '/' + site.name;
ui.showModal(_('Setting Homepage'), [E('p', { 'class': 'spinning' }, _('Renaming...'))]);
fs.read(path + '/' + filename).then(function(content) {
return fs.write(path + '/index.html', content);
}).then(function() {
return fs.remove(path + '/' + filename);
}).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', filename + _(' set as homepage')));
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
deleteFile: function(site, filename) {
var self = this;
var sitesRoot = '/srv/metablogizer/sites';
if (!confirm(_('Delete ') + filename + '?')) return;
fs.remove(sitesRoot + '/' + site.name + '/' + filename).then(function() {
ui.addNotification(null, E('p', _('File deleted')));
self.showFilesModal(site);
}).catch(function(e) {
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
showShareModal: function(site) {
var self = this;
var url = site.url || ('https://' + site.domain);
var title = site.name + ' - SecuBox';
var enc = encodeURIComponent;
var qrSvg = '';
try {
qrSvg = qrcode.generateSVG(url, 180);
} catch (e) {
qrSvg = '<p>QR code unavailable</p>';
}
ui.showModal(_('Share: ') + site.name, [
E('div', { 'style': 'text-align:center' }, [
E('div', { 'class': 'cbi-value', 'style': 'display:flex; gap:0.5em; margin-bottom:1em' }, [
E('input', { 'type': 'text', 'readonly': true, 'value': url, 'id': 'share-url',
'class': 'cbi-input-text', 'style': 'flex:1' }),
E('button', { 'class': 'cbi-button cbi-button-action', 'click': function() {
self.copyToClipboard(url);
}}, _('Copy'))
]),
E('div', { 'style': 'display:inline-block; padding:1em; background:#f8f8f8; border-radius:8px; margin:1em 0' }, [
E('div', { 'innerHTML': qrSvg })
]),
E('div', { 'style': 'margin-top:1em' }, [
E('p', { 'style': 'margin-bottom:0.5em' }, _('Share on:')),
E('div', { 'style': 'display:flex; gap:0.5em; justify-content:center; flex-wrap:wrap' }, [
E('a', { 'href': 'https://twitter.com/intent/tweet?url=' + enc(url) + '&text=' + enc(title),
'target': '_blank', 'class': 'cbi-button cbi-button-action' }, 'Twitter'),
E('a', { 'href': 'https://www.linkedin.com/sharing/share-offsite/?url=' + enc(url),
'target': '_blank', 'class': 'cbi-button cbi-button-action' }, 'LinkedIn'),
E('a', { 'href': 'https://t.me/share/url?url=' + enc(url) + '&text=' + enc(title),
'target': '_blank', 'class': 'cbi-button cbi-button-action' }, 'Telegram'),
E('a', { 'href': 'https://wa.me/?text=' + enc(title + ' ' + url),
'target': '_blank', 'class': 'cbi-button cbi-button-action' }, 'WhatsApp'),
E('a', { 'href': 'mailto:?subject=' + enc(title) + '&body=' + enc(url),
'class': 'cbi-button cbi-button-action' }, 'Email')
])
])
]),
E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [
E('a', { 'href': url, 'target': '_blank', 'class': 'cbi-button cbi-button-positive',
'style': 'text-decoration:none' }, _('Visit Site')),
' ',
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close'))
])
]);
},
handleSync: function(site) {
ui.showModal(_('Syncing'), [E('p', { 'class': 'spinning' }, _('Pulling from Gitea...'))]);
api.syncSite(site.id).then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', _('Site synced successfully')));
} else {
ui.addNotification(null, E('p', _('Sync failed: ') + (r.error || 'Unknown')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
handleDelete: function(site) {
var self = this;
ui.showModal(_('Delete Site'), [
E('p', {}, _('Are you sure you want to delete "') + site.name + '"?'),
E('p', { 'style': 'color:#a00' }, _('This will remove the site, HAProxy vhost, and all files.')),
E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
' ',
E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() {
ui.hideModal();
ui.showModal(_('Deleting'), [E('p', { 'class': 'spinning' }, _('Removing site...'))]);
api.deleteSite(site.id).then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', _('Site deleted')));
window.location.reload();
} else {
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
}}, _('Delete'))
])
]);
},
copyToClipboard: function(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
ui.addNotification(null, E('p', _('URL copied to clipboard')));
});
} else {
var input = document.getElementById('share-url');
if (input) {
input.select();
document.execCommand('copy');
ui.addNotification(null, E('p', _('URL copied to clipboard')));
}
}
},
handleSyncConfig: function() {
ui.showModal(_('Syncing Configuration'), [
E('p', { 'class': 'spinning' }, _('Updating port and runtime info for all sites...'))
]);
api.syncConfig().then(function(r) {
ui.hideModal();
if (r.success) {
var msg = _('Configuration synced');
if (r.fixed > 0) {
msg += ' (' + r.fixed + _(' entries updated)');
}
ui.addNotification(null, E('p', msg));
window.location.reload();
} else {
ui.addNotification(null, E('p', _('Sync failed: ') + (r.error || 'Unknown')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
});
},
formatSize: function(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});