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>
377 lines
10 KiB
JavaScript
377 lines
10 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require poll';
|
|
'require rpc';
|
|
'require ui';
|
|
|
|
var callMediaHubStatus = rpc.declare({
|
|
object: 'luci.media-hub',
|
|
method: 'status',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
var callMediaHubServices = rpc.declare({
|
|
object: 'luci.media-hub',
|
|
method: 'services',
|
|
expect: { services: [] }
|
|
});
|
|
|
|
var callServiceStart = rpc.declare({
|
|
object: 'luci.media-hub',
|
|
method: 'service_start',
|
|
params: ['id']
|
|
});
|
|
|
|
var callServiceStop = rpc.declare({
|
|
object: 'luci.media-hub',
|
|
method: 'service_stop',
|
|
params: ['id']
|
|
});
|
|
|
|
var callServiceRestart = rpc.declare({
|
|
object: 'luci.media-hub',
|
|
method: 'service_restart',
|
|
params: ['id']
|
|
});
|
|
|
|
// Category icons and colors
|
|
var categoryConfig = {
|
|
streaming: { icon: '🎬', color: '#e74c3c', label: 'Streaming' },
|
|
conferencing: { icon: '📹', color: '#3498db', label: 'Conferencing' },
|
|
apps: { icon: '📊', color: '#9b59b6', label: 'Apps' },
|
|
display: { icon: '🪞', color: '#1abc9c', label: 'Display' },
|
|
social: { icon: '🦣', color: '#e67e22', label: 'Social' },
|
|
monitoring: { icon: '📡', color: '#2ecc71', label: 'Monitoring' }
|
|
};
|
|
|
|
// Status colors and icons
|
|
var statusConfig = {
|
|
running: { color: '#27ae60', icon: '●', label: 'Running' },
|
|
stopped: { color: '#e74c3c', icon: '○', label: 'Stopped' },
|
|
not_installed: { color: '#95a5a6', icon: '◌', label: 'Not Installed' },
|
|
unknown: { color: '#f39c12', icon: '?', label: 'Unknown' }
|
|
};
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callMediaHubStatus(),
|
|
callMediaHubServices()
|
|
]);
|
|
},
|
|
|
|
renderStatusBar: function(status) {
|
|
var total = status.total_services || 0;
|
|
var installed = status.installed || 0;
|
|
var running = status.running || 0;
|
|
var stopped = status.stopped || 0;
|
|
|
|
return E('div', { 'class': 'media-hub-status-bar' }, [
|
|
E('div', { 'class': 'status-item' }, [
|
|
E('span', { 'class': 'status-value', 'style': 'color: #3498db' }, String(total)),
|
|
E('span', { 'class': 'status-label' }, 'Total')
|
|
]),
|
|
E('div', { 'class': 'status-item' }, [
|
|
E('span', { 'class': 'status-value', 'style': 'color: #9b59b6' }, String(installed)),
|
|
E('span', { 'class': 'status-label' }, 'Installed')
|
|
]),
|
|
E('div', { 'class': 'status-item' }, [
|
|
E('span', { 'class': 'status-value', 'style': 'color: #27ae60' }, String(running)),
|
|
E('span', { 'class': 'status-label' }, 'Running')
|
|
]),
|
|
E('div', { 'class': 'status-item' }, [
|
|
E('span', { 'class': 'status-value', 'style': 'color: #e74c3c' }, String(stopped)),
|
|
E('span', { 'class': 'status-label' }, 'Stopped')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderServiceCard: function(service) {
|
|
var self = this;
|
|
var statusCfg = statusConfig[service.status] || statusConfig.unknown;
|
|
var categoryCfg = categoryConfig[service.category] || { icon: '📦', color: '#7f8c8d', label: 'Other' };
|
|
|
|
var controls = [];
|
|
|
|
if (service.installed) {
|
|
if (service.status === 'running') {
|
|
controls.push(
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-negative',
|
|
'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return callServiceStop(service.id).then(function() {
|
|
window.location.reload();
|
|
});
|
|
})
|
|
}, '⏹ Stop'),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'style': 'padding: 4px 12px; font-size: 12px;',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return callServiceRestart(service.id).then(function() {
|
|
window.location.reload();
|
|
});
|
|
})
|
|
}, '🔄 Restart')
|
|
);
|
|
} else if (service.status === 'stopped') {
|
|
controls.push(
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-positive',
|
|
'style': 'padding: 4px 12px; font-size: 12px;',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return callServiceStart(service.id).then(function() {
|
|
window.location.reload();
|
|
});
|
|
})
|
|
}, '▶ Start')
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add settings link
|
|
if (service.url) {
|
|
controls.push(
|
|
E('a', {
|
|
'href': service.url,
|
|
'class': 'cbi-button cbi-button-neutral',
|
|
'style': 'margin-left: 5px; padding: 4px 12px; font-size: 12px; text-decoration: none;'
|
|
}, '⚙ Settings')
|
|
);
|
|
}
|
|
|
|
return E('div', {
|
|
'class': 'media-service-card',
|
|
'data-status': service.status,
|
|
'style': 'border-left: 4px solid ' + categoryCfg.color + ';'
|
|
}, [
|
|
E('div', { 'class': 'card-header' }, [
|
|
E('span', { 'class': 'service-emoji' }, service.emoji || '📦'),
|
|
E('div', { 'class': 'service-info' }, [
|
|
E('span', { 'class': 'service-name' }, service.name),
|
|
E('span', { 'class': 'service-category', 'style': 'color: ' + categoryCfg.color }, categoryCfg.label)
|
|
]),
|
|
E('span', {
|
|
'class': 'service-status',
|
|
'style': 'color: ' + statusCfg.color,
|
|
'title': statusCfg.label
|
|
}, statusCfg.icon + ' ' + statusCfg.label)
|
|
]),
|
|
E('div', { 'class': 'card-body' }, [
|
|
E('p', { 'class': 'service-description' }, service.description),
|
|
service.port > 0 ? E('p', { 'class': 'service-port' }, 'Port: ' + service.port) : E('span')
|
|
]),
|
|
E('div', { 'class': 'card-footer' }, controls)
|
|
]);
|
|
},
|
|
|
|
renderCategorySection: function(category, services) {
|
|
var categoryCfg = categoryConfig[category] || { icon: '📦', color: '#7f8c8d', label: category };
|
|
var categoryServices = services.filter(function(s) { return s.category === category; });
|
|
|
|
if (categoryServices.length === 0) return E('span');
|
|
|
|
var self = this;
|
|
return E('div', { 'class': 'media-category-section' }, [
|
|
E('h3', { 'class': 'category-title', 'style': 'color: ' + categoryCfg.color }, [
|
|
E('span', { 'class': 'category-icon' }, categoryCfg.icon),
|
|
' ',
|
|
categoryCfg.label,
|
|
E('span', { 'class': 'category-count' }, ' (' + categoryServices.length + ')')
|
|
]),
|
|
E('div', { 'class': 'media-cards-grid' },
|
|
categoryServices.map(function(service) {
|
|
return self.renderServiceCard(service);
|
|
})
|
|
)
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0];
|
|
var services = data[1];
|
|
|
|
// Sort by category then by name
|
|
services.sort(function(a, b) {
|
|
if (a.category !== b.category) {
|
|
return a.category.localeCompare(b.category);
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
// Get unique categories in order
|
|
var categories = ['streaming', 'conferencing', 'apps', 'display', 'social', 'monitoring'];
|
|
|
|
var self = this;
|
|
var view = E('div', { 'class': 'media-hub-dashboard' }, [
|
|
E('style', {}, `
|
|
.media-hub-dashboard {
|
|
padding: 20px;
|
|
}
|
|
.media-hub-header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
.media-hub-header h2 {
|
|
font-size: 2em;
|
|
margin-bottom: 10px;
|
|
}
|
|
.media-hub-header .subtitle {
|
|
color: #666;
|
|
font-size: 1.1em;
|
|
}
|
|
.media-hub-status-bar {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 40px;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
border-radius: 12px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.status-item {
|
|
text-align: center;
|
|
}
|
|
.status-value {
|
|
display: block;
|
|
font-size: 2.5em;
|
|
font-weight: bold;
|
|
}
|
|
.status-label {
|
|
color: #aaa;
|
|
font-size: 0.9em;
|
|
text-transform: uppercase;
|
|
}
|
|
.media-category-section {
|
|
margin-bottom: 30px;
|
|
}
|
|
.category-title {
|
|
font-size: 1.4em;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #333;
|
|
}
|
|
.category-icon {
|
|
font-size: 1.2em;
|
|
}
|
|
.category-count {
|
|
font-weight: normal;
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
}
|
|
.media-cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.media-service-card {
|
|
background: #1a1a2e;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
.media-service-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
|
}
|
|
.media-service-card[data-status="running"] {
|
|
background: linear-gradient(135deg, #1a2e1a 0%, #162e16 100%);
|
|
}
|
|
.media-service-card[data-status="stopped"] {
|
|
background: linear-gradient(135deg, #2e1a1a 0%, #2e1616 100%);
|
|
}
|
|
.media-service-card[data-status="not_installed"] {
|
|
opacity: 0.7;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
.service-emoji {
|
|
font-size: 2.5em;
|
|
margin-right: 15px;
|
|
}
|
|
.service-info {
|
|
flex: 1;
|
|
}
|
|
.service-name {
|
|
display: block;
|
|
font-size: 1.3em;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
.service-category {
|
|
font-size: 0.85em;
|
|
text-transform: uppercase;
|
|
}
|
|
.service-status {
|
|
font-size: 0.9em;
|
|
font-weight: bold;
|
|
}
|
|
.card-body {
|
|
margin-bottom: 15px;
|
|
}
|
|
.service-description {
|
|
color: #aaa;
|
|
font-size: 0.95em;
|
|
margin-bottom: 8px;
|
|
}
|
|
.service-port {
|
|
color: #666;
|
|
font-size: 0.85em;
|
|
font-family: monospace;
|
|
}
|
|
.card-footer {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
.card-footer button, .card-footer a {
|
|
border-radius: 6px;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.media-hub-status-bar {
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
}
|
|
.media-cards-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
`),
|
|
E('div', { 'class': 'media-hub-header' }, [
|
|
E('h2', {}, '🎬 Media Services Hub'),
|
|
E('p', { 'class': 'subtitle' }, 'Unified dashboard for all SecuBox media services')
|
|
]),
|
|
this.renderStatusBar(status)
|
|
]);
|
|
|
|
categories.forEach(function(category) {
|
|
var section = self.renderCategorySection(category, services);
|
|
if (section.tagName !== 'SPAN') {
|
|
view.appendChild(section);
|
|
}
|
|
});
|
|
|
|
// Setup polling for status updates
|
|
poll.add(L.bind(function() {
|
|
return callMediaHubServices().then(L.bind(function(services) {
|
|
// Update status indicators without full reload
|
|
services.forEach(function(service) {
|
|
var card = document.querySelector('.media-service-card[data-status]');
|
|
// Could update individual cards here for smooth updates
|
|
});
|
|
}, this));
|
|
}, this), 30);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|