secubox-openwrt/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/static.js
CyberMind-FR 58220065b5 feat(v0.23.0): Matrix homeserver, SaaS Relay CDN caching, Media Hub dashboard
Matrix Homeserver (Conduit):
- E2EE mesh messaging using Conduit v0.10.12 in LXC container
- matrixctl CLI: install/uninstall, user/room management, federation
- luci-app-matrix: status cards, user form, emancipate, mesh publish
- RPCD backend with 17 methods
- Identity (DID) integration and P2P mesh publication

SaaS Relay CDN Caching & Session Replay:
- CDN cache profiles: minimal, gandalf (default), aggressive
- Session replay modes: shared, per_user, master
- saasctl cache/session commands for management
- Enhanced mitmproxy addon (415 lines) with response caching

Media Services Hub Dashboard:
- Unified dashboard at /admin/services/media-hub
- Category-organized cards (streaming, conferencing, apps, etc.)
- Service status indicators with start/stop/restart controls
- RPCD backend querying 8 media services

Also includes:
- HexoJS static upload workflow and multi-user auth
- Jitsi config.js Promise handling fix
- Feed package updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 11:44:26 +01:00

397 lines
12 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require form';
'require fs';
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({
selectedInstance: null,
load: function() {
return callStaticList({});
},
renderInstanceCard: function(instance) {
var self = this;
var card = E('div', { 'class': 'cbi-section', 'style': 'margin-bottom: 10px; padding: 15px; border: 1px solid #ccc; border-radius: 8px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;' }, [
E('div', {}, [
E('strong', { 'style': 'font-size: 1.2em;' }, instance.name),
instance.domain ? E('span', { 'style': 'margin-left: 10px; color: #666;' }, instance.domain) : '',
instance.auth_enabled ? E('span', { 'class': 'badge', 'style': 'margin-left: 10px; background: #28a745; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.8em;' }, 'Auth') : ''
]),
E('div', {}, [
E('span', { 'style': 'margin-right: 15px; color: #666;' }, instance.file_count + ' files'),
instance.port > 0 ? E('span', { 'style': 'color: #666;' }, 'Port ' + instance.port) : ''
])
]),
E('div', { 'style': 'display: flex; gap: 10px;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() { self.showFiles(instance.name); }
}, 'Manage Files'),
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': function() { self.publishSite(instance.name); }
}, 'Publish'),
E('button', {
'class': 'cbi-button',
'click': function() { self.configureAuth(instance.name, instance.domain); }
}, 'Auth'),
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.deleteSite(instance.name); }
}, 'Delete')
])
]);
return card;
},
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', {}, 'Files in "' + instanceName + '"'));
// Upload section
var uploadSection = E('div', { 'class': 'cbi-section', 'style': 'padding: 15px; background: #f9f9f9; border-radius: 8px; margin-bottom: 15px;' }, [
E('h4', {}, 'Upload Files'),
E('input', {
'type': 'file',
'id': 'file-upload-input',
'multiple': true,
'style': 'margin-right: 10px;'
}),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() { self.uploadFiles(instanceName); }
}, 'Upload Selected')
]);
container.appendChild(uploadSection);
// File list table
var table = E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Filename'),
E('th', { 'class': 'th' }, 'Size'),
E('th', { 'class': 'th' }, 'Modified'),
E('th', { 'class': 'th' }, 'Actions')
])
]);
var files = result.files || [];
if (files.length === 0) {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'colspan': '4', 'style': 'text-align: center; color: #666;' }, 'No files yet. Upload some files above.')
]));
} else {
files.forEach(function(file) {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, file.name),
E('td', { 'class': 'td' }, formatBytes(file.size)),
E('td', { 'class': 'td' }, formatDate(file.modified)),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'style': 'padding: 2px 8px; font-size: 0.9em;',
'click': function() { self.deleteFile(instanceName, file.name); }
}, 'Delete')
])
]));
});
}
container.appendChild(table);
// Back button
container.appendChild(E('button', {
'class': 'cbi-button',
'style': 'margin-top: 15px;',
'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) {
// Convert to base64
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;
// Validate name
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: red;' }, result.error || 'Failed to load sites'));
return;
}
var instances = result.instances || [];
if (instances.length === 0) {
instancesContainer.appendChild(E('p', { 'style': 'color: #666; 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 view = E('div', { 'class': 'cbi-map' }, [
E('h2', { 'class': 'cbi-map-title' }, 'Static Sites'),
E('div', { 'class': 'cbi-map-descr' },
'Upload and manage static HTML sites with optional Basic Auth via HAProxy. Fast KISS publishing without Hexo build process.'),
// Create button
E('div', { 'style': 'margin: 20px 0;' }, [
E('button', {
'class': 'cbi-button cbi-button-add',
'click': function() { self.createSite(); }
}, 'Create New Site')
]),
// Instances container
E('div', { 'id': 'static-instances' }),
// File list container (shown when managing a site)
E('div', { 'id': 'file-list-container', 'style': 'margin-top: 20px;' })
]);
// Render initial instances
var instancesContainer = view.querySelector('#static-instances');
if (instances.length === 0) {
instancesContainer.appendChild(E('p', { 'style': 'color: #666; 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));
});
}
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});