Add secubox-app-gotosocial and luci-app-gotosocial for running a lightweight ActivityPub social network server in LXC container. Features: - gotosocialctl CLI with install, start, stop, user management - LXC container deployment (ARM64) - HAProxy integration via emancipate command - UCI configuration for instance, container, proxy, federation settings - LuCI web interface with overview, users, and settings tabs - Mesh integration support for auto-federation between SecuBox nodes - Backup/restore functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require poll';
|
|
|
|
var callStatus = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'status',
|
|
expect: {}
|
|
});
|
|
|
|
var callInstall = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'install',
|
|
expect: {}
|
|
});
|
|
|
|
var callStart = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'start',
|
|
expect: {}
|
|
});
|
|
|
|
var callStop = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'stop',
|
|
expect: {}
|
|
});
|
|
|
|
var callRestart = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'restart',
|
|
expect: {}
|
|
});
|
|
|
|
var callEmancipate = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'emancipate',
|
|
params: ['domain', 'tor', 'dns', 'mesh'],
|
|
expect: {}
|
|
});
|
|
|
|
var callRevoke = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'revoke',
|
|
expect: {}
|
|
});
|
|
|
|
var callBackup = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'backup',
|
|
expect: {}
|
|
});
|
|
|
|
var callLogs = rpc.declare({
|
|
object: 'luci.gotosocial',
|
|
method: 'logs',
|
|
params: ['lines'],
|
|
expect: {}
|
|
});
|
|
|
|
return view.extend({
|
|
status: null,
|
|
|
|
load: function() {
|
|
return callStatus();
|
|
},
|
|
|
|
pollStatus: function() {
|
|
return callStatus().then(L.bind(function(status) {
|
|
this.status = status;
|
|
this.updateStatusDisplay(status);
|
|
}, this));
|
|
},
|
|
|
|
updateStatusDisplay: function(status) {
|
|
var containerEl = document.getElementById('container-status');
|
|
var serviceEl = document.getElementById('service-status');
|
|
var versionEl = document.getElementById('gts-version');
|
|
var hostEl = document.getElementById('gts-host');
|
|
var exposureEl = document.getElementById('exposure-status');
|
|
|
|
if (containerEl) {
|
|
if (status.container_running) {
|
|
containerEl.textContent = 'Running';
|
|
containerEl.className = 'badge success';
|
|
} else if (status.installed) {
|
|
containerEl.textContent = 'Stopped';
|
|
containerEl.className = 'badge warning';
|
|
} else {
|
|
containerEl.textContent = 'Not Installed';
|
|
containerEl.className = 'badge danger';
|
|
}
|
|
}
|
|
|
|
if (serviceEl) {
|
|
if (status.service_running) {
|
|
serviceEl.textContent = 'Running';
|
|
serviceEl.className = 'badge success';
|
|
} else {
|
|
serviceEl.textContent = 'Stopped';
|
|
serviceEl.className = 'badge warning';
|
|
}
|
|
}
|
|
|
|
if (versionEl) {
|
|
versionEl.textContent = status.version || '-';
|
|
}
|
|
|
|
if (hostEl) {
|
|
hostEl.textContent = status.host || '-';
|
|
}
|
|
|
|
if (exposureEl) {
|
|
var channels = [];
|
|
if (status.tor_enabled) channels.push('Tor');
|
|
if (status.dns_enabled) channels.push('DNS/SSL');
|
|
if (status.mesh_enabled) channels.push('Mesh');
|
|
exposureEl.textContent = channels.length > 0 ? channels.join(', ') : 'None';
|
|
}
|
|
|
|
// Update button states
|
|
var installBtn = document.getElementById('btn-install');
|
|
var startBtn = document.getElementById('btn-start');
|
|
var stopBtn = document.getElementById('btn-stop');
|
|
var restartBtn = document.getElementById('btn-restart');
|
|
|
|
if (installBtn) installBtn.disabled = status.installed;
|
|
if (startBtn) startBtn.disabled = !status.installed || status.container_running;
|
|
if (stopBtn) stopBtn.disabled = !status.container_running;
|
|
if (restartBtn) restartBtn.disabled = !status.container_running;
|
|
},
|
|
|
|
handleInstall: function() {
|
|
return ui.showModal(_('Install GoToSocial'), [
|
|
E('p', _('This will download and install GoToSocial in an LXC container. This may take several minutes.')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action important',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
ui.hideModal();
|
|
ui.showModal(_('Installing...'), [
|
|
E('p', { 'class': 'spinning' }, _('Installing GoToSocial, please wait...'))
|
|
]);
|
|
return callInstall().then(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', _('GoToSocial installed successfully')), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Installation failed')), 'error');
|
|
}
|
|
return callStatus();
|
|
}).then(L.bind(function(status) {
|
|
this.updateStatusDisplay(status);
|
|
}, this));
|
|
})
|
|
}, _('Install'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleStart: function() {
|
|
ui.showModal(_('Starting...'), [
|
|
E('p', { 'class': 'spinning' }, _('Starting GoToSocial...'))
|
|
]);
|
|
return callStart().then(L.bind(function(res) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', res.message || _('GoToSocial started')), 'success');
|
|
return this.pollStatus();
|
|
}, this));
|
|
},
|
|
|
|
handleStop: function() {
|
|
return callStop().then(L.bind(function(res) {
|
|
ui.addNotification(null, E('p', res.message || _('GoToSocial stopped')), 'info');
|
|
return this.pollStatus();
|
|
}, this));
|
|
},
|
|
|
|
handleRestart: function() {
|
|
ui.showModal(_('Restarting...'), [
|
|
E('p', { 'class': 'spinning' }, _('Restarting GoToSocial...'))
|
|
]);
|
|
return callRestart().then(L.bind(function(res) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', res.message || _('GoToSocial restarted')), 'success');
|
|
return this.pollStatus();
|
|
}, this));
|
|
},
|
|
|
|
handleEmancipate: function() {
|
|
var domain = this.status && this.status.host ? this.status.host : '';
|
|
|
|
return ui.showModal(_('Expose Service'), [
|
|
E('p', _('Configure exposure channels for your Fediverse instance.')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', { 'type': 'text', 'id': 'emancipate-domain', 'class': 'cbi-input-text', 'value': domain })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Channels')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('label', { 'style': 'display:block' }, [
|
|
E('input', { 'type': 'checkbox', 'id': 'emancipate-tor' }), ' ', _('Tor (.onion)')
|
|
]),
|
|
E('label', { 'style': 'display:block' }, [
|
|
E('input', { 'type': 'checkbox', 'id': 'emancipate-dns', 'checked': true }), ' ', _('DNS/SSL (HTTPS)')
|
|
]),
|
|
E('label', { 'style': 'display:block' }, [
|
|
E('input', { 'type': 'checkbox', 'id': 'emancipate-mesh' }), ' ', _('Mesh Network')
|
|
])
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
var domain = document.getElementById('emancipate-domain').value;
|
|
var tor = document.getElementById('emancipate-tor').checked;
|
|
var dns = document.getElementById('emancipate-dns').checked;
|
|
var mesh = document.getElementById('emancipate-mesh').checked;
|
|
|
|
ui.hideModal();
|
|
ui.showModal(_('Exposing...'), [
|
|
E('p', { 'class': 'spinning' }, _('Setting up exposure channels...'))
|
|
]);
|
|
|
|
return callEmancipate(domain, tor, dns, mesh).then(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('Service exposed successfully')), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Exposure failed')), 'error');
|
|
}
|
|
});
|
|
})
|
|
}, _('Expose'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleRevoke: function() {
|
|
return ui.showModal(_('Revoke Exposure'), [
|
|
E('p', _('This will remove all exposure channels for GoToSocial. The service will no longer be accessible externally.')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
ui.hideModal();
|
|
return callRevoke().then(function(res) {
|
|
ui.addNotification(null, E('p', res.message || _('Exposure revoked')), 'info');
|
|
});
|
|
})
|
|
}, _('Revoke'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleBackup: function() {
|
|
ui.showModal(_('Creating Backup...'), [
|
|
E('p', { 'class': 'spinning' }, _('Creating backup...'))
|
|
]);
|
|
return callBackup().then(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || _('Backup created')), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || _('Backup failed')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
handleViewLogs: function() {
|
|
return callLogs(100).then(function(res) {
|
|
var logs = res.logs || [];
|
|
ui.showModal(_('GoToSocial Logs'), [
|
|
E('div', { 'style': 'max-height:400px; overflow-y:auto; font-family:monospace; font-size:12px; background:#111; color:#0f0; padding:10px; white-space:pre-wrap;' },
|
|
logs.join('\n') || _('No logs available')
|
|
),
|
|
E('div', { 'class': 'right', 'style': 'margin-top:10px' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
|
|
])
|
|
]);
|
|
});
|
|
},
|
|
|
|
render: function(status) {
|
|
this.status = status;
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', _('GoToSocial Fediverse Server')),
|
|
E('div', { 'class': 'cbi-map-descr' }, _('Lightweight ActivityPub social network server for the Fediverse.')),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', _('Status')),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td', 'width': '200px' }, _('Container')),
|
|
E('td', { 'class': 'td' }, E('span', { 'id': 'container-status', 'class': 'badge' }, '-'))
|
|
]),
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, _('Service')),
|
|
E('td', { 'class': 'td' }, E('span', { 'id': 'service-status', 'class': 'badge' }, '-'))
|
|
]),
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, _('Version')),
|
|
E('td', { 'class': 'td' }, E('span', { 'id': 'gts-version' }, '-'))
|
|
]),
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, _('Domain')),
|
|
E('td', { 'class': 'td' }, E('span', { 'id': 'gts-host' }, '-'))
|
|
]),
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, _('Exposure')),
|
|
E('td', { 'class': 'td' }, E('span', { 'id': 'exposure-status' }, '-'))
|
|
])
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', _('Actions')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('button', {
|
|
'id': 'btn-install',
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, this.handleInstall)
|
|
}, _('Install')),
|
|
' ',
|
|
E('button', {
|
|
'id': 'btn-start',
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, this.handleStart)
|
|
}, _('Start')),
|
|
' ',
|
|
E('button', {
|
|
'id': 'btn-stop',
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleStop)
|
|
}, _('Stop')),
|
|
' ',
|
|
E('button', {
|
|
'id': 'btn-restart',
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleRestart)
|
|
}, _('Restart'))
|
|
]),
|
|
E('div', { 'class': 'cbi-value', 'style': 'margin-top:10px' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': ui.createHandlerFn(this, this.handleEmancipate)
|
|
}, _('Expose Service')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleRevoke)
|
|
}, _('Revoke Exposure')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleBackup)
|
|
}, _('Backup')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-neutral',
|
|
'click': ui.createHandlerFn(this, this.handleViewLogs)
|
|
}, _('View Logs'))
|
|
])
|
|
]),
|
|
|
|
E('style', {}, `
|
|
.badge { padding: 2px 8px; border-radius: 3px; font-weight: bold; }
|
|
.badge.success { background: #4CAF50; color: white; }
|
|
.badge.warning { background: #FF9800; color: white; }
|
|
.badge.danger { background: #f44336; color: white; }
|
|
`)
|
|
]);
|
|
|
|
this.updateStatusDisplay(status);
|
|
poll.add(L.bind(this.pollStatus, this), 5);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|