'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; // Load hosting status asynchronously after render api.getHostingStatus().then(function(h) { self.hosting = h || {}; var haproxyEl = document.getElementById('haproxy-status'); var ipEl = document.getElementById('public-ip'); if (haproxyEl) { haproxyEl.innerHTML = ''; haproxyEl.appendChild(h.haproxy_status === 'running' ? E('span', { 'style': 'color:#0a0' }, _('Running')) : E('span', { 'style': 'color:#a00' }, _('Stopped'))); } if (ipEl) { ipEl.textContent = h.public_ip || '-'; } }).catch(function() {}); 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', 'id': 'haproxy-status' }, E('em', {}, _('Loading...'))) ]), E('tr', { 'class': 'tr' }, [ E('td', { 'class': 'td' }, _('Public IP')), E('td', { 'class': 'td', 'id': 'public-ip' }, _('Loading...')) ]), 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...'))]); // Process files sequentially to avoid RPC batch conflicts var uploadSequential = function(files, idx, results) { if (idx >= files.length) { return Promise.resolve(results); } var f = files[idx]; return new Promise(function(resolve) { var reader = new FileReader(); reader.onload = function(e) { // Convert ArrayBuffer to base64 var bytes = new Uint8Array(e.target.result); var chunks = []; for (var i = 0; i < bytes.length; i += 8192) { chunks.push(String.fromCharCode.apply(null, bytes.slice(i, i + 8192))); } var content = btoa(chunks.join('')); var dest = (asIndex && f === firstHtml) ? 'index.html' : f.name; // Use chunked upload for files > 40KB (uhttpd has 64KB JSON body limit) var uploadFn; if (content.length > 40000) { uploadFn = api.chunkedUpload(site.id, dest, content); } else { uploadFn = api.uploadFile(site.id, dest, content); } uploadFn .then(function(r) { results.push({ ok: r && r.success, name: f.name }); resolve(); }) .catch(function() { results.push({ ok: false, name: f.name }); resolve(); }); }; reader.onerror = function() { results.push({ ok: false, name: f.name }); resolve(); }; reader.readAsArrayBuffer(f); }).then(function() { return uploadSequential(files, idx + 1, results); }); }; uploadSequential(this.uploadFiles, 0, []).then(function(results) { ui.hideModal(); var ok = results.filter(function(r) { return r.ok; }).length; var failed = results.length - ok; if (failed > 0) { ui.addNotification(null, E('p', ok + _(' file(s) uploaded, ') + failed + _(' failed')), 'warning'); } else { 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 = '
QR code unavailable
'; } 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 });