- 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>
696 lines
25 KiB
JavaScript
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
|
|
});
|