Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
433 lines
13 KiB
JavaScript
433 lines
13 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require form';
|
|
'require fs';
|
|
'require secubox/kiss-theme';
|
|
|
|
var callStaticList = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_list',
|
|
params: ['instance'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticUpload = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_upload',
|
|
params: ['instance', 'filename', 'content'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticCreate = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_create',
|
|
params: ['name', 'domain'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticDelete = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_delete',
|
|
params: ['name'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticPublish = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_publish',
|
|
params: ['instance'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticDeleteFile = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_delete_file',
|
|
params: ['instance', 'filename'],
|
|
expect: {}
|
|
});
|
|
|
|
var callStaticConfigureAuth = rpc.declare({
|
|
object: 'luci.hexojs',
|
|
method: 'static_configure_auth',
|
|
params: ['instance', 'enabled', 'domain'],
|
|
expect: {}
|
|
});
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var k = 1024;
|
|
var sizes = ['B', 'KB', 'MB', 'GB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(timestamp) {
|
|
if (!timestamp) return '-';
|
|
var d = new Date(timestamp * 1000);
|
|
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
|
|
}
|
|
|
|
return view.extend({
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null,
|
|
|
|
selectedInstance: null,
|
|
|
|
load: function() {
|
|
return callStaticList({});
|
|
},
|
|
|
|
renderStats: function(instances) {
|
|
var c = KissTheme.colors;
|
|
var totalFiles = instances.reduce(function(sum, inst) { return sum + (inst.file_count || 0); }, 0);
|
|
var withAuth = instances.filter(function(inst) { return inst.auth_enabled; }).length;
|
|
return [
|
|
KissTheme.stat(instances.length, 'Sites', c.blue),
|
|
KissTheme.stat(totalFiles, 'Files', c.cyan),
|
|
KissTheme.stat(withAuth, 'Auth', c.green)
|
|
];
|
|
},
|
|
|
|
renderInstanceCard: function(instance) {
|
|
var self = this;
|
|
return E('div', {
|
|
'style': 'background: var(--kiss-bg); border-radius: 8px; padding: 16px; margin-bottom: 12px;'
|
|
}, [
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
|
|
E('strong', { 'style': 'font-size: 1.1em;' }, instance.name),
|
|
instance.domain ? E('span', { 'style': 'color: var(--kiss-muted);' }, instance.domain) : '',
|
|
instance.auth_enabled ? KissTheme.badge('Auth', 'green') : ''
|
|
]),
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px; color: var(--kiss-muted); font-size: 12px;' }, [
|
|
E('span', {}, instance.file_count + ' files'),
|
|
instance.port > 0 ? E('span', {}, 'Port ' + instance.port) : ''
|
|
])
|
|
]),
|
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'style': 'padding: 6px 12px; font-size: 12px;',
|
|
'click': function() { self.showFiles(instance.name); }
|
|
}, 'Manage Files'),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'style': 'padding: 6px 12px; font-size: 12px;',
|
|
'click': function() { self.publishSite(instance.name); }
|
|
}, 'Publish'),
|
|
E('button', {
|
|
'class': 'kiss-btn',
|
|
'style': 'padding: 6px 12px; font-size: 12px;',
|
|
'click': function() { self.configureAuth(instance.name, instance.domain); }
|
|
}, 'Auth'),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-red',
|
|
'style': 'padding: 6px 12px; font-size: 12px;',
|
|
'click': function() { self.deleteSite(instance.name); }
|
|
}, 'Delete')
|
|
])
|
|
]);
|
|
},
|
|
|
|
showFiles: function(instanceName) {
|
|
var self = this;
|
|
|
|
callStaticList({ instance: instanceName }).then(function(result) {
|
|
if (!result.success) {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to list files'), 'error');
|
|
return;
|
|
}
|
|
|
|
self.selectedInstance = instanceName;
|
|
var container = document.getElementById('file-list-container');
|
|
container.innerHTML = '';
|
|
|
|
// Header
|
|
container.appendChild(E('h3', { 'style': 'margin: 0 0 16px 0;' }, 'Files in "' + instanceName + '"'));
|
|
|
|
// Upload section
|
|
var uploadSection = E('div', {
|
|
'style': 'padding: 16px; background: var(--kiss-bg); border-radius: 8px; margin-bottom: 16px;'
|
|
}, [
|
|
E('h4', { 'style': 'margin: 0 0 12px 0; font-size: 14px;' }, 'Upload Files'),
|
|
E('div', { 'style': 'display: flex; gap: 12px; align-items: center;' }, [
|
|
E('input', {
|
|
'type': 'file',
|
|
'id': 'file-upload-input',
|
|
'multiple': true,
|
|
'style': 'color: var(--kiss-text);'
|
|
}),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': function() { self.uploadFiles(instanceName); }
|
|
}, 'Upload Selected')
|
|
])
|
|
]);
|
|
container.appendChild(uploadSection);
|
|
|
|
// File list table
|
|
var files = result.files || [];
|
|
if (files.length === 0) {
|
|
container.appendChild(E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' },
|
|
'No files yet. Upload some files above.'));
|
|
} else {
|
|
var rows = files.map(function(file) {
|
|
return E('tr', {}, [
|
|
E('td', {}, file.name),
|
|
E('td', { 'style': 'color: var(--kiss-muted);' }, formatBytes(file.size)),
|
|
E('td', { 'style': 'color: var(--kiss-muted);' }, formatDate(file.modified)),
|
|
E('td', { 'style': 'width: 100px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-red',
|
|
'style': 'padding: 4px 10px; font-size: 11px;',
|
|
'click': function() { self.deleteFile(instanceName, file.name); }
|
|
}, 'Delete')
|
|
])
|
|
]);
|
|
});
|
|
|
|
container.appendChild(E('table', { 'class': 'kiss-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'Filename'),
|
|
E('th', {}, 'Size'),
|
|
E('th', {}, 'Modified'),
|
|
E('th', {}, 'Actions')
|
|
])
|
|
]),
|
|
E('tbody', {}, rows)
|
|
]));
|
|
}
|
|
|
|
// Back button
|
|
container.appendChild(E('button', {
|
|
'class': 'kiss-btn',
|
|
'style': 'margin-top: 16px;',
|
|
'click': function() { self.refreshView(); }
|
|
}, 'Back to Sites'));
|
|
});
|
|
},
|
|
|
|
uploadFiles: function(instanceName) {
|
|
var self = this;
|
|
var input = document.getElementById('file-upload-input');
|
|
var files = input.files;
|
|
|
|
if (files.length === 0) {
|
|
ui.addNotification(null, E('p', 'Please select files to upload'), 'warning');
|
|
return;
|
|
}
|
|
|
|
var uploadPromises = [];
|
|
|
|
for (var i = 0; i < files.length; i++) {
|
|
(function(file) {
|
|
var promise = new Promise(function(resolve, reject) {
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(e.target.result)));
|
|
|
|
callStaticUpload({
|
|
instance: instanceName,
|
|
filename: file.name,
|
|
content: base64
|
|
}).then(function(result) {
|
|
if (result.success) {
|
|
resolve(file.name);
|
|
} else {
|
|
reject(result.error || 'Upload failed for ' + file.name);
|
|
}
|
|
}).catch(reject);
|
|
};
|
|
reader.onerror = function() {
|
|
reject('Failed to read file: ' + file.name);
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
});
|
|
uploadPromises.push(promise);
|
|
})(files[i]);
|
|
}
|
|
|
|
Promise.all(uploadPromises).then(function(uploaded) {
|
|
ui.addNotification(null, E('p', 'Uploaded ' + uploaded.length + ' file(s)'), 'success');
|
|
self.showFiles(instanceName);
|
|
}).catch(function(err) {
|
|
ui.addNotification(null, E('p', 'Upload error: ' + err), 'error');
|
|
self.showFiles(instanceName);
|
|
});
|
|
},
|
|
|
|
deleteFile: function(instanceName, filename) {
|
|
var self = this;
|
|
|
|
if (!confirm('Delete file "' + filename + '"?')) return;
|
|
|
|
callStaticDeleteFile({ instance: instanceName, filename: filename }).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', 'File deleted'), 'success');
|
|
self.showFiles(instanceName);
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to delete file'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
publishSite: function(instanceName) {
|
|
callStaticPublish({ instance: instanceName }).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', 'Published to ' + result.url), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to publish'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
configureAuth: function(instanceName, currentDomain) {
|
|
var self = this;
|
|
|
|
var domain = prompt('Enter domain for HAProxy auth (e.g., site.example.com):', currentDomain || '');
|
|
if (domain === null) return;
|
|
|
|
if (!domain) {
|
|
ui.addNotification(null, E('p', 'Domain is required for HAProxy auth'), 'warning');
|
|
return;
|
|
}
|
|
|
|
callStaticConfigureAuth({
|
|
instance: instanceName,
|
|
enabled: true,
|
|
domain: domain
|
|
}).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', 'Auth configured for ' + domain), 'success');
|
|
self.refreshView();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to configure auth'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteSite: function(instanceName) {
|
|
var self = this;
|
|
|
|
if (!confirm('Delete static site "' + instanceName + '" and all its files?')) return;
|
|
|
|
callStaticDelete({ name: instanceName }).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', 'Site deleted'), 'success');
|
|
self.refreshView();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to delete site'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
createSite: function() {
|
|
var self = this;
|
|
|
|
var name = prompt('Enter site name (lowercase, no spaces):');
|
|
if (!name) return;
|
|
|
|
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
|
|
ui.addNotification(null, E('p', 'Invalid name. Use lowercase letters, numbers, underscore.'), 'error');
|
|
return;
|
|
}
|
|
|
|
var domain = prompt('Enter domain (optional, e.g., site.example.com):');
|
|
|
|
callStaticCreate({ name: name, domain: domain || '' }).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', 'Site "' + name + '" created on port ' + result.port), 'success');
|
|
self.refreshView();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || 'Failed to create site'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
refreshView: function() {
|
|
var self = this;
|
|
|
|
callStaticList({}).then(function(result) {
|
|
var container = document.getElementById('file-list-container');
|
|
container.innerHTML = '';
|
|
|
|
var instancesContainer = document.getElementById('static-instances');
|
|
instancesContainer.innerHTML = '';
|
|
|
|
if (!result.success) {
|
|
instancesContainer.appendChild(E('p', { 'style': 'color: var(--kiss-red);' }, result.error || 'Failed to load sites'));
|
|
return;
|
|
}
|
|
|
|
var instances = result.instances || [];
|
|
if (instances.length === 0) {
|
|
instancesContainer.appendChild(E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' },
|
|
'No static sites yet. Click "Create New Site" to get started.'));
|
|
} else {
|
|
instances.forEach(function(inst) {
|
|
instancesContainer.appendChild(self.renderInstanceCard(inst));
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var instances = (data && data.instances) || [];
|
|
|
|
var content = [
|
|
// Header
|
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Static Sites'),
|
|
KissTheme.badge(instances.length + ' Sites', 'blue')
|
|
]),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
|
'Upload and manage static HTML sites with optional Basic Auth via HAProxy. Fast KISS publishing without Hexo build process.')
|
|
]),
|
|
|
|
// Stats
|
|
E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'margin: 20px 0;' },
|
|
this.renderStats(instances)),
|
|
|
|
// Create button
|
|
E('div', { 'style': 'margin: 20px 0;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'click': function() { self.createSite(); }
|
|
}, 'Create New Site')
|
|
]),
|
|
|
|
// Instances container
|
|
KissTheme.card('Sites', E('div', { 'id': 'static-instances' })),
|
|
|
|
// File list container
|
|
E('div', { 'id': 'file-list-container', 'style': 'margin-top: 20px;' })
|
|
];
|
|
|
|
// Render initial instances
|
|
var instancesContainer = content[4].querySelector ? content[4].querySelector('#static-instances') : null;
|
|
|
|
// Defer rendering after DOM is available
|
|
setTimeout(function() {
|
|
var el = document.getElementById('static-instances');
|
|
if (el) {
|
|
if (instances.length === 0) {
|
|
el.appendChild(E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' },
|
|
'No static sites yet. Click "Create New Site" to get started.'));
|
|
} else {
|
|
instances.forEach(function(inst) {
|
|
el.appendChild(self.renderInstanceCard(inst));
|
|
});
|
|
}
|
|
}
|
|
}, 0);
|
|
|
|
return KissTheme.wrap(content, 'admin/services/hexojs/static');
|
|
}
|
|
});
|