feat(nextcloud): Migrate to LXC with full-stack enhancement
- Migrate from Docker to Debian 12 LXC container - Full stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud - Rewrite nextcloudctl CLI with install/backup/restore/ssl/occ commands - New UCI config schema: main, db, redis, ssl, backup sections - Enhanced RPCD backend with 15 methods - KISS dashboard with Overview/Backups/SSL/Logs tabs - Updated dependencies for LXC packages - SecuBox menu path integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
59d0e89a8c
commit
09b40c3b88
@ -1861,3 +1861,26 @@ git checkout HEAD -- index.html
|
||||
- MetaBlogizer: HAProxy vhost lookup for automatic subdomain detection
|
||||
- Added more icons for new service types
|
||||
- **Result**: 67 services with proper subdomain URLs
|
||||
|
||||
### 2026-02-16: Nextcloud LXC Enhancement
|
||||
- **Migrated** secubox-app-nextcloud from Docker to LXC (Debian 12 based)
|
||||
- **Complete rewrite** of `nextcloudctl` CLI (1018 lines):
|
||||
- Commands: install, uninstall, update, status, logs, shell, occ, backup, restore, ssl-enable, ssl-disable
|
||||
- Downloads Debian 12 rootfs from LXC image server
|
||||
- Installs full stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud
|
||||
- Automated database setup and configuration
|
||||
- **New UCI config schema** with sections: main, db, redis, ssl, backup
|
||||
- **Enhanced RPCD backend** (366 lines) with 15 methods:
|
||||
- status, get_config, save_config, install, start, stop, restart
|
||||
- update, backup, restore, list_backups, ssl_enable, ssl_disable, occ, logs
|
||||
- **KISS Dashboard** (725 lines) with:
|
||||
- Install view with feature cards
|
||||
- Overview tab with stats grid (Status, Version, Users, Storage)
|
||||
- Backups tab with create/restore functionality
|
||||
- SSL tab for HAProxy/ACME integration
|
||||
- Logs tab for operation monitoring
|
||||
- **Updated dependencies**:
|
||||
- secubox-app-nextcloud: +lxc +lxc-common +tar +wget-ssl +jsonfilter +openssl-util +unzip +xz
|
||||
- luci-app-nextcloud: +luci-lib-secubox +secubox-app-nextcloud
|
||||
- **Updated ACL** with all new RPCD methods
|
||||
- **Updated menu** to SecuBox path (admin/secubox/services/nextcloud)
|
||||
|
||||
@ -62,7 +62,16 @@ _Last updated: 2026-02-15 (PeerTube transcoding fix, GK2 Hub subdomain URLs)_
|
||||
- Gossip-based exposure config sync via secubox-p2p
|
||||
- Created `luci-app-vortex-dns` dashboard
|
||||
|
||||
### Just Completed (2026-02-15)
|
||||
### Just Completed (2026-02-16)
|
||||
|
||||
- **Nextcloud LXC Enhancement** — DONE (2026-02-16)
|
||||
- Migrated from Docker to Debian 12 LXC container
|
||||
- Full-stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud
|
||||
- `nextcloudctl` CLI with install/backup/restore/ssl/occ commands
|
||||
- KISS dashboard with Overview/Backups/SSL/Logs tabs
|
||||
- RPCD backend with 15 methods
|
||||
|
||||
### Recently Completed (2026-02-15)
|
||||
|
||||
- **HAProxy & Mitmproxy WAF Fixes** — DONE (2026-02-15)
|
||||
- Fixed HAProxy reload: copy config to `/etc/haproxy/` before signal
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Nextcloud
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_TITLE:=LuCI support for Nextcloud LXC
|
||||
LUCI_DEPENDS:=+luci-base +luci-lib-secubox +secubox-app-nextcloud
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-nextcloud
|
||||
|
||||
@ -1,214 +1,722 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
'require secubox/kiss-theme';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.nextcloud', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.nextcloud', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.nextcloud', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.nextcloud', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.nextcloud', method: 'restart', expect: {} });
|
||||
// ============================================================================
|
||||
// RPC Declarations
|
||||
// ============================================================================
|
||||
|
||||
var css = '.nc-container{max-width:900px;margin:0 auto}.nc-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#0082c9 0%,#00639b 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.nc-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.nc-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.nc-status.running{background:rgba(16,185,129,.2)}.nc-status.stopped{background:rgba(239,68,68,.2)}.nc-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.nc-status.running .nc-dot{background:#10b981}.nc-status.stopped .nc-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.nc-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.nc-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.nc-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}.nc-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.nc-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.nc-info-value{font-size:1.1rem;font-weight:500}.nc-actions{display:flex;gap:.75rem;flex-wrap:wrap}.nc-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.nc-btn-primary{background:linear-gradient(135deg,#0082c9,#00639b);color:#fff}.nc-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,130,201,.3)}.nc-btn-success{background:#10b981;color:#fff}.nc-btn-danger{background:#ef4444;color:#fff}.nc-btn-secondary{background:#6b7280;color:#fff}.nc-btn:disabled{opacity:.5;cursor:not-allowed}.nc-webui{display:flex;align-items:center;gap:1rem;padding:1rem;background:linear-gradient(135deg,rgba(0,130,201,.1),rgba(0,99,155,.1));border-radius:12px;margin-top:1rem}.nc-webui-icon{font-size:2rem}.nc-webui-info{flex:1}.nc-webui-url{font-family:monospace;color:#0082c9}.nc-not-installed{text-align:center;padding:3rem}.nc-not-installed h3{margin-bottom:1rem;color:#333}.nc-not-installed p{color:#666;margin-bottom:1.5rem}.nc-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin:1.5rem 0;text-align:left}.nc-feature{padding:.75rem;background:#f0f9ff;border-radius:8px;font-size:.9rem}';
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'status',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callGetConfig = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'get_config',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callInstall = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'install',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callStart = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'start',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callStop = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'stop',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRestart = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'restart',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callUpdate = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'update',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callBackup = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'backup',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRestore = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'restore',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callListBackups = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'list_backups',
|
||||
expect: { backups: [] }
|
||||
});
|
||||
|
||||
var callSSLEnable = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'ssl_enable',
|
||||
params: ['domain'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSSLDisable = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'ssl_disable',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callLogs = rpc.declare({
|
||||
object: 'luci.nextcloud',
|
||||
method: 'logs',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
function fmtDate(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
var d = new Date(timestamp * 1000);
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
function fmtRelative(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
var d = new Date(timestamp * 1000);
|
||||
var now = new Date();
|
||||
var diff = Math.floor((now - d) / 1000);
|
||||
if (diff < 60) return diff + 's ago';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main View
|
||||
// ============================================================================
|
||||
|
||||
return view.extend({
|
||||
pollActive: true,
|
||||
status: {},
|
||||
config: {},
|
||||
backups: [],
|
||||
currentTab: 'overview',
|
||||
|
||||
load: function() {
|
||||
return callStatus();
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
var self = this;
|
||||
this.pollActive = true;
|
||||
poll.add(L.bind(function() {
|
||||
if (!this.pollActive) return Promise.resolve();
|
||||
return callStatus().then(L.bind(function(status) {
|
||||
this.updateStatus(status);
|
||||
}, this));
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
updateStatus: function(status) {
|
||||
var badge = document.querySelector('.nc-status');
|
||||
var statusText = document.querySelector('.nc-status-text');
|
||||
|
||||
if (badge && statusText) {
|
||||
badge.className = 'nc-status ' + (status.running ? 'running' : 'stopped');
|
||||
statusText.textContent = status.running ? _('Running') : _('Stopped');
|
||||
}
|
||||
},
|
||||
|
||||
handleInstall: function() {
|
||||
var self = this;
|
||||
ui.showModal(_('Installing Nextcloud'), [
|
||||
E('p', { 'class': 'spinning' }, _('Installing Nextcloud. This may take several minutes...'))
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callGetConfig(),
|
||||
callListBackups().catch(function() { return { backups: [] }; })
|
||||
]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
self.startPolling();
|
||||
} 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');
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
this.status = data[0] || {};
|
||||
this.config = data[1] || {};
|
||||
this.backups = (data[2] || {}).backups || [];
|
||||
|
||||
// Not installed - show install view
|
||||
if (!this.status.installed) {
|
||||
return this.renderInstallView();
|
||||
}
|
||||
|
||||
// Tab navigation
|
||||
var tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: '🎛️' },
|
||||
{ id: 'backups', label: 'Backups', icon: '💾' },
|
||||
{ id: 'ssl', label: 'SSL', icon: '🔒' },
|
||||
{ id: 'logs', label: 'Logs', icon: '📜' }
|
||||
];
|
||||
|
||||
var content = [
|
||||
// Header
|
||||
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;' }, [
|
||||
E('div', {}, [
|
||||
E('h1', { 'style': 'font-size:28px;font-weight:700;margin:0;display:flex;align-items:center;gap:12px;' }, [
|
||||
'☁️ Nextcloud'
|
||||
]),
|
||||
E('p', { 'style': 'color:var(--kiss-muted);margin:6px 0 0;' }, 'Self-hosted file sync and collaboration platform')
|
||||
]),
|
||||
E('div', { 'style': 'display:flex;gap:8px;' }, [
|
||||
KissTheme.badge(this.status.version || 'N/A', 'blue'),
|
||||
KissTheme.badge(this.status.running ? 'Running' : 'Stopped',
|
||||
this.status.running ? 'green' : 'red')
|
||||
])
|
||||
]),
|
||||
|
||||
// Tab Navigation
|
||||
E('div', { 'class': 'kiss-tabs', 'style': 'margin-bottom:20px;' },
|
||||
tabs.map(function(tab) {
|
||||
return E('button', {
|
||||
'class': 'kiss-tab' + (self.currentTab === tab.id ? ' kiss-tab-active' : ''),
|
||||
'data-tab': tab.id,
|
||||
'click': function() { self.switchTab(tab.id); }
|
||||
}, [tab.icon + ' ' + tab.label]);
|
||||
})
|
||||
),
|
||||
|
||||
// Tab Content
|
||||
E('div', { 'id': 'tab-content' }, this.renderTabContent())
|
||||
];
|
||||
|
||||
poll.add(L.bind(this.refresh, this), 10);
|
||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
||||
},
|
||||
|
||||
switchTab: function(tabId) {
|
||||
this.currentTab = tabId;
|
||||
var tabContent = document.getElementById('tab-content');
|
||||
if (tabContent) {
|
||||
dom.content(tabContent, this.renderTabContent());
|
||||
}
|
||||
document.querySelectorAll('.kiss-tab').forEach(function(btn) {
|
||||
btn.classList.toggle('kiss-tab-active', btn.dataset.tab === tabId);
|
||||
});
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Nextcloud...'))]);
|
||||
callStart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud started')));
|
||||
else ui.addNotification(null, E('p', _('Failed to start')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
renderTabContent: function() {
|
||||
switch (this.currentTab) {
|
||||
case 'backups': return this.renderBackupsTab();
|
||||
case 'ssl': return this.renderSSLTab();
|
||||
case 'logs': return this.renderLogsTab();
|
||||
default: return this.renderOverviewTab();
|
||||
}
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping Nextcloud...'))]);
|
||||
callStop().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud stopped')));
|
||||
else ui.addNotification(null, E('p', _('Failed to stop')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
// ========================================================================
|
||||
// Install View
|
||||
// ========================================================================
|
||||
|
||||
handleRestart: function() {
|
||||
ui.showModal(_('Restarting...'), [E('p', { 'class': 'spinning' }, _('Restarting Nextcloud...'))]);
|
||||
callRestart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud restarted')));
|
||||
else ui.addNotification(null, E('p', _('Failed to restart')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
renderInstallView: function() {
|
||||
var self = this;
|
||||
|
||||
if (!document.getElementById('nc-styles')) {
|
||||
var s = document.createElement('style');
|
||||
s.id = 'nc-styles';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
var content = [
|
||||
E('div', { 'style': 'text-align:center;padding:60px 20px;' }, [
|
||||
E('div', { 'style': 'font-size:80px;margin-bottom:24px;' }, '☁️'),
|
||||
E('h1', { 'style': 'font-size:32px;margin:0 0 12px;' }, 'Nextcloud'),
|
||||
E('p', { 'style': 'color:var(--kiss-muted);font-size:18px;max-width:500px;margin:0 auto 32px;' },
|
||||
'Self-hosted productivity platform with file sync, calendar, contacts, and more.'),
|
||||
|
||||
// Not installed view
|
||||
if (!status.installed || !status.docker_available) {
|
||||
var content = E('div', { 'class': 'nc-container' }, [
|
||||
E('div', { 'class': 'nc-header' }, [
|
||||
E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]),
|
||||
E('div', { 'class': 'nc-status stopped' }, [
|
||||
E('span', { 'class': 'nc-dot' }),
|
||||
E('span', { 'class': 'nc-status-text' }, _('Not Installed'))
|
||||
E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:16px;max-width:600px;margin:0 auto 40px;' }, [
|
||||
this.featureCard('📁', 'File Sync'),
|
||||
this.featureCard('📅', 'Calendar'),
|
||||
this.featureCard('👥', 'Contacts'),
|
||||
this.featureCard('📝', 'Documents'),
|
||||
this.featureCard('📷', 'Photos'),
|
||||
this.featureCard('🔐', 'E2E Encryption')
|
||||
]),
|
||||
|
||||
E('div', { 'style': 'background:var(--kiss-bg2);border-radius:12px;padding:20px;max-width:500px;margin:0 auto 32px;text-align:left;' }, [
|
||||
E('div', { 'style': 'font-weight:600;margin-bottom:12px;' }, '📦 What will be installed:'),
|
||||
E('ul', { 'style': 'margin:0;padding-left:20px;color:var(--kiss-muted);font-size:14px;' }, [
|
||||
E('li', {}, 'Debian 12 LXC container'),
|
||||
E('li', {}, 'Nextcloud with PHP 8.2'),
|
||||
E('li', {}, 'MariaDB database'),
|
||||
E('li', {}, 'Redis caching'),
|
||||
E('li', {}, 'Nginx web server')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\u2601\ufe0f'),
|
||||
E('h3', {}, _('Nextcloud')),
|
||||
E('p', {}, _('Self-hosted productivity platform with file sync, calendar, contacts, and more.')),
|
||||
E('div', { 'class': 'nc-features' }, [
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcc1 ' + _('File Sync')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcc5 ' + _('Calendar')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udc65 ' + _('Contacts')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcdd ' + _('Documents')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcf7 ' + _('Photos')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udd12 ' + _('E2E Encryption'))
|
||||
]),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin-bottom:1rem' }, _('Docker is required but not available')) : '',
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'handleInstall'),
|
||||
'disabled': !status.docker_available
|
||||
}, _('Install Nextcloud'))
|
||||
])
|
||||
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-green',
|
||||
'style': 'font-size:18px;padding:16px 48px;',
|
||||
'click': function() { self.handleInstall(); }
|
||||
}, '🚀 Install Nextcloud')
|
||||
])
|
||||
];
|
||||
|
||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
||||
},
|
||||
|
||||
featureCard: function(icon, label) {
|
||||
return E('div', { 'style': 'background:var(--kiss-bg2);border-radius:10px;padding:16px;text-align:center;' }, [
|
||||
E('div', { 'style': 'font-size:28px;margin-bottom:8px;' }, icon),
|
||||
E('div', { 'style': 'font-size:13px;font-weight:500;' }, label)
|
||||
]);
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Overview Tab
|
||||
// ========================================================================
|
||||
|
||||
renderOverviewTab: function() {
|
||||
var self = this;
|
||||
var s = this.status;
|
||||
|
||||
return E('div', {}, [
|
||||
// Stats Grid
|
||||
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom:24px;' }, [
|
||||
KissTheme.stat(s.running ? 'Online' : 'Offline', 'Status', s.running ? 'var(--kiss-green)' : 'var(--kiss-red)'),
|
||||
KissTheme.stat(s.version || 'N/A', 'Version', 'var(--kiss-blue)'),
|
||||
KissTheme.stat(s.user_count || 0, 'Users', 'var(--kiss-purple)'),
|
||||
KissTheme.stat(s.disk_used || '0B', 'Storage', 'var(--kiss-cyan)')
|
||||
]),
|
||||
|
||||
// Quick Actions
|
||||
KissTheme.card([
|
||||
E('span', {}, '⚡ Quick Actions')
|
||||
], E('div', { 'style': 'display:flex;gap:12px;flex-wrap:wrap;' }, [
|
||||
E('button', {
|
||||
'class': 'kiss-btn ' + (s.running ? 'kiss-btn-red' : 'kiss-btn-green'),
|
||||
'click': function() { self.handleToggle(); }
|
||||
}, s.running ? ['⏹️ ', 'Stop'] : ['▶️ ', 'Start']),
|
||||
E('button', {
|
||||
'class': 'kiss-btn',
|
||||
'click': function() { self.handleRestart(); },
|
||||
'disabled': !s.running
|
||||
}, ['🔄 ', 'Restart']),
|
||||
s.web_accessible ? E('a', {
|
||||
'href': s.web_url,
|
||||
'target': '_blank',
|
||||
'class': 'kiss-btn kiss-btn-blue'
|
||||
}, ['🌐 ', 'Open Nextcloud']) : null,
|
||||
E('button', {
|
||||
'class': 'kiss-btn',
|
||||
'click': function() { self.handleUpdate(); }
|
||||
}, ['⬆️ ', 'Update']),
|
||||
E('button', {
|
||||
'class': 'kiss-btn',
|
||||
'click': function() { self.handleQuickBackup(); }
|
||||
}, ['💾 ', 'Backup Now'])
|
||||
].filter(Boolean))),
|
||||
|
||||
// Service Info
|
||||
KissTheme.card([
|
||||
E('span', {}, 'ℹ️ Service Information')
|
||||
], E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap:16px;' }, [
|
||||
E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [
|
||||
this.infoRow('Container', s.container_name || 'nextcloud'),
|
||||
this.infoRow('HTTP Port', s.http_port || 8080),
|
||||
this.infoRow('Domain', s.domain || 'cloud.local'),
|
||||
this.infoRow('Data Path', s.data_path || '/srv/nextcloud')
|
||||
]),
|
||||
E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [
|
||||
this.infoRow('SSL', s.ssl_enabled ? '✅ Enabled' : '❌ Disabled'),
|
||||
this.infoRow('SSL Domain', s.ssl_domain || '-'),
|
||||
this.infoRow('Web Accessible', s.web_accessible ? '✅ Yes' : '❌ No'),
|
||||
this.infoRow('Enabled', s.enabled ? '✅ Yes' : '❌ No')
|
||||
])
|
||||
])),
|
||||
|
||||
// Web Access Card
|
||||
s.running && s.web_url ? KissTheme.card([
|
||||
E('span', {}, '🌐 Web Interface')
|
||||
], E('div', { 'style': 'display:flex;align-items:center;gap:16px;' }, [
|
||||
E('div', { 'style': 'flex:1;' }, [
|
||||
E('div', { 'style': 'font-size:14px;color:var(--kiss-muted);margin-bottom:4px;' }, 'Access your Nextcloud at:'),
|
||||
E('div', { 'style': 'font-family:monospace;font-size:16px;color:var(--kiss-cyan);' }, s.web_url)
|
||||
]),
|
||||
E('a', {
|
||||
'href': s.web_url,
|
||||
'target': '_blank',
|
||||
'class': 'kiss-btn kiss-btn-green'
|
||||
}, ['🔗 ', 'Open'])
|
||||
])) : null
|
||||
].filter(Boolean));
|
||||
},
|
||||
|
||||
infoRow: function(label, value) {
|
||||
return E('div', { 'style': 'display:flex;justify-content:space-between;padding:8px;background:var(--kiss-bg2);border-radius:6px;' }, [
|
||||
E('span', { 'style': 'color:var(--kiss-muted);' }, label),
|
||||
E('span', { 'style': 'font-weight:500;' }, String(value))
|
||||
]);
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Backups Tab
|
||||
// ========================================================================
|
||||
|
||||
renderBackupsTab: function() {
|
||||
var self = this;
|
||||
|
||||
return E('div', {}, [
|
||||
// Create Backup
|
||||
KissTheme.card([
|
||||
E('span', {}, '➕ Create Backup')
|
||||
], E('div', { 'style': 'display:flex;gap:12px;align-items:center;' }, [
|
||||
E('input', {
|
||||
'id': 'backup-name',
|
||||
'type': 'text',
|
||||
'placeholder': 'Backup name (optional)',
|
||||
'style': 'flex:1;padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);'
|
||||
}),
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-green',
|
||||
'click': function() {
|
||||
var name = document.getElementById('backup-name')?.value || '';
|
||||
self.handleBackup(name);
|
||||
}
|
||||
}, ['💾 ', 'Create Backup'])
|
||||
])),
|
||||
|
||||
// Backup List
|
||||
KissTheme.card([
|
||||
E('span', {}, '📦 Available Backups'),
|
||||
E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.backups.length + ' backups')
|
||||
], E('div', { 'id': 'backups-list' }, this.renderBackupsList()))
|
||||
]);
|
||||
},
|
||||
|
||||
renderBackupsList: function() {
|
||||
var self = this;
|
||||
|
||||
if (!this.backups.length) {
|
||||
return E('div', { 'style': 'text-align:center;padding:40px;color:var(--kiss-muted);' }, [
|
||||
E('div', { 'style': 'font-size:48px;margin-bottom:12px;' }, '💾'),
|
||||
E('div', { 'style': 'font-size:16px;' }, 'No backups yet'),
|
||||
E('div', { 'style': 'font-size:12px;margin-top:8px;' }, 'Create a backup to protect your data')
|
||||
]);
|
||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
||||
}
|
||||
|
||||
// Installed view
|
||||
this.startPolling();
|
||||
return E('table', { 'class': 'kiss-table' }, [
|
||||
E('thead', {}, E('tr', {}, [
|
||||
E('th', {}, 'Name'),
|
||||
E('th', {}, 'Size'),
|
||||
E('th', {}, 'Date'),
|
||||
E('th', {}, 'Actions')
|
||||
])),
|
||||
E('tbody', {}, this.backups.map(function(b) {
|
||||
return E('tr', {}, [
|
||||
E('td', { 'style': 'font-family:monospace;' }, b.name),
|
||||
E('td', {}, b.size || '-'),
|
||||
E('td', {}, fmtRelative(b.timestamp)),
|
||||
E('td', {}, E('button', {
|
||||
'class': 'kiss-btn kiss-btn-blue',
|
||||
'style': 'padding:4px 10px;font-size:11px;',
|
||||
'data-name': b.name,
|
||||
'click': function(ev) { self.handleRestore(ev.currentTarget.dataset.name); }
|
||||
}, '⬇️ Restore'))
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
|
||||
var content = E('div', { 'class': 'nc-container' }, [
|
||||
E('div', { 'class': 'nc-header' }, [
|
||||
E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]),
|
||||
E('div', { 'class': 'nc-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'nc-dot' }),
|
||||
E('span', { 'class': 'nc-status-text' }, status.running ? _('Running') : _('Stopped'))
|
||||
])
|
||||
]),
|
||||
// ========================================================================
|
||||
// SSL Tab
|
||||
// ========================================================================
|
||||
|
||||
// Info Card
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-card-title' }, ['\u2139\ufe0f ', _('Service Information')]),
|
||||
E('div', { 'class': 'nc-info-grid' }, [
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Port')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.port || '80')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Admin User')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.admin_user || 'admin')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Trusted Domains')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.trusted_domains || 'cloud.local')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Data Path')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.data_path || '/srv/nextcloud')
|
||||
])
|
||||
renderSSLTab: function() {
|
||||
var self = this;
|
||||
var s = this.status;
|
||||
|
||||
return E('div', {}, [
|
||||
// SSL Status
|
||||
KissTheme.card([
|
||||
E('span', {}, '🔒 SSL Status')
|
||||
], E('div', { 'style': 'display:flex;align-items:center;gap:16px;' }, [
|
||||
E('div', { 'style': 'font-size:48px;' }, s.ssl_enabled ? '🔐' : '🔓'),
|
||||
E('div', { 'style': 'flex:1;' }, [
|
||||
E('div', { 'style': 'font-size:20px;font-weight:600;' }, s.ssl_enabled ? 'SSL Enabled' : 'SSL Disabled'),
|
||||
s.ssl_enabled && s.ssl_domain ? E('div', { 'style': 'color:var(--kiss-muted);margin-top:4px;' }, 'Domain: ' + s.ssl_domain) : null
|
||||
]),
|
||||
s.ssl_enabled ? E('button', {
|
||||
'class': 'kiss-btn kiss-btn-red',
|
||||
'click': function() { self.handleSSLDisable(); }
|
||||
}, '🔓 Disable SSL') : null
|
||||
].filter(Boolean))),
|
||||
|
||||
// Web UI Link
|
||||
status.running && status.web_accessible ? E('div', { 'class': 'nc-webui' }, [
|
||||
E('div', { 'class': 'nc-webui-icon' }, '\ud83c\udf10'),
|
||||
E('div', { 'class': 'nc-webui-info' }, [
|
||||
E('div', { 'style': 'font-weight:600' }, _('Web Interface')),
|
||||
E('div', { 'class': 'nc-webui-url' }, status.web_url)
|
||||
]),
|
||||
E('a', {
|
||||
'href': status.web_url,
|
||||
'target': '_blank',
|
||||
'class': 'nc-btn nc-btn-primary'
|
||||
}, _('Open'))
|
||||
]) : ''
|
||||
]),
|
||||
|
||||
// Actions Card
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-card-title' }, ['\u26a1 ', _('Actions')]),
|
||||
E('div', { 'class': 'nc-actions' }, [
|
||||
// Enable SSL Form
|
||||
!s.ssl_enabled ? KissTheme.card([
|
||||
E('span', {}, '🌐 Enable SSL via HAProxy')
|
||||
], E('div', {}, [
|
||||
E('p', { 'style': 'color:var(--kiss-muted);margin-bottom:16px;' },
|
||||
'Configure HTTPS access with automatic Let\'s Encrypt certificates via HAProxy.'),
|
||||
E('div', { 'style': 'display:flex;gap:12px;align-items:center;' }, [
|
||||
E('input', {
|
||||
'id': 'ssl-domain',
|
||||
'type': 'text',
|
||||
'placeholder': 'cloud.example.com',
|
||||
'style': 'flex:1;padding:12px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);font-size:14px;'
|
||||
}),
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-success',
|
||||
'click': ui.createHandlerFn(this, 'handleStart'),
|
||||
'disabled': status.running
|
||||
}, _('Start')),
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-danger',
|
||||
'click': ui.createHandlerFn(this, 'handleStop'),
|
||||
'disabled': !status.running
|
||||
}, _('Stop')),
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-secondary',
|
||||
'click': ui.createHandlerFn(this, 'handleRestart'),
|
||||
'disabled': !status.running
|
||||
}, _('Restart')),
|
||||
E('a', {
|
||||
'href': L.url('admin', 'secubox', 'services', 'nextcloud', 'settings'),
|
||||
'class': 'nc-btn nc-btn-secondary'
|
||||
}, _('Settings'))
|
||||
'class': 'kiss-btn kiss-btn-green',
|
||||
'click': function() {
|
||||
var domain = document.getElementById('ssl-domain')?.value;
|
||||
if (domain) self.handleSSLEnable(domain);
|
||||
else ui.addNotification(null, E('p', 'Enter a domain name'), 'warning');
|
||||
}
|
||||
}, ['🔐 ', 'Enable SSL'])
|
||||
]),
|
||||
E('div', { 'style': 'margin-top:16px;padding:12px;background:var(--kiss-bg2);border-radius:6px;' }, [
|
||||
E('div', { 'style': 'font-weight:600;margin-bottom:8px;color:var(--kiss-yellow);' }, '⚠️ Prerequisites:'),
|
||||
E('ul', { 'style': 'margin:0;padding-left:20px;color:var(--kiss-muted);font-size:13px;' }, [
|
||||
E('li', {}, 'Domain must point to this server\'s public IP'),
|
||||
E('li', {}, 'Port 80 and 443 must be accessible'),
|
||||
E('li', {}, 'HAProxy must be installed and running')
|
||||
])
|
||||
])
|
||||
])) : null
|
||||
].filter(Boolean));
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Logs Tab
|
||||
// ========================================================================
|
||||
|
||||
renderLogsTab: function() {
|
||||
var self = this;
|
||||
|
||||
return E('div', {}, [
|
||||
KissTheme.card([
|
||||
E('span', {}, '📜 Installation/Operation Logs'),
|
||||
E('button', {
|
||||
'class': 'kiss-btn',
|
||||
'style': 'margin-left:auto;padding:4px 10px;font-size:11px;',
|
||||
'click': function() { self.refreshLogs(); }
|
||||
}, '🔄 Refresh')
|
||||
], E('pre', {
|
||||
'id': 'logs-content',
|
||||
'style': 'background:#0a0a0a;color:#0f0;padding:16px;border-radius:8px;font-size:11px;height:400px;overflow-y:auto;margin:0;font-family:monospace;white-space:pre-wrap;'
|
||||
}, '(Loading logs...)'))
|
||||
]);
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Action Handlers
|
||||
// ========================================================================
|
||||
|
||||
handleInstall: function() {
|
||||
var self = this;
|
||||
ui.showModal('Installing Nextcloud', [
|
||||
E('div', { 'style': 'text-align:center;padding:20px;' }, [
|
||||
E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, '⏳'),
|
||||
E('p', { 'style': 'margin-top:16px;' }, 'Installing Nextcloud LXC container...'),
|
||||
E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take several minutes. Please wait.')
|
||||
])
|
||||
]);
|
||||
|
||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || 'Installation started'), 'info');
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleToggle: function() {
|
||||
var self = this;
|
||||
var running = this.status.running;
|
||||
|
||||
ui.showModal(running ? 'Stopping...' : 'Starting...', [
|
||||
E('p', { 'class': 'spinning' }, (running ? 'Stopping' : 'Starting') + ' Nextcloud...')
|
||||
]);
|
||||
|
||||
var fn = running ? callStop : callStart;
|
||||
fn().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', running ? 'Nextcloud stopped' : 'Nextcloud started'), 'info');
|
||||
self.refresh();
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleRestart: function() {
|
||||
var self = this;
|
||||
ui.showModal('Restarting...', [
|
||||
E('p', { 'class': 'spinning' }, 'Restarting Nextcloud...')
|
||||
]);
|
||||
|
||||
callRestart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', 'Nextcloud restarted'), 'info');
|
||||
self.refresh();
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleUpdate: function() {
|
||||
var self = this;
|
||||
ui.showModal('Updating Nextcloud', [
|
||||
E('div', { 'style': 'text-align:center;padding:20px;' }, [
|
||||
E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, '⬆️'),
|
||||
E('p', { 'style': 'margin-top:16px;' }, 'Updating Nextcloud...'),
|
||||
E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take a few minutes.')
|
||||
])
|
||||
]);
|
||||
|
||||
callUpdate().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || 'Update started'), 'info');
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleQuickBackup: function() {
|
||||
this.handleBackup('');
|
||||
},
|
||||
|
||||
handleBackup: function(name) {
|
||||
var self = this;
|
||||
ui.showModal('Creating Backup', [
|
||||
E('p', { 'class': 'spinning' }, 'Creating backup...')
|
||||
]);
|
||||
|
||||
callBackup(name || null).then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', 'Backup created: ' + (r.backup_name || 'done')), 'info');
|
||||
self.refreshBackups();
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleRestore: function(name) {
|
||||
var self = this;
|
||||
if (!confirm('Restore from backup "' + name + '"? This will stop Nextcloud and may take several minutes.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal('Restoring Backup', [
|
||||
E('div', { 'style': 'text-align:center;padding:20px;' }, [
|
||||
E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, '⬇️'),
|
||||
E('p', { 'style': 'margin-top:16px;' }, 'Restoring from ' + name + '...'),
|
||||
E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take several minutes.')
|
||||
])
|
||||
]);
|
||||
|
||||
callRestore(name).then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || 'Restore started'), 'info');
|
||||
self.refresh();
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleSSLEnable: function(domain) {
|
||||
var self = this;
|
||||
ui.showModal('Enabling SSL', [
|
||||
E('p', { 'class': 'spinning' }, 'Configuring SSL for ' + domain + '...')
|
||||
]);
|
||||
|
||||
callSSLEnable(domain).then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || 'SSL enabled'), 'info');
|
||||
self.refresh();
|
||||
} 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');
|
||||
});
|
||||
},
|
||||
|
||||
handleSSLDisable: function() {
|
||||
var self = this;
|
||||
if (!confirm('Disable SSL? HTTPS access will no longer work.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callSSLDisable().then(function(r) {
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', 'SSL disabled'), 'info');
|
||||
self.refresh();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown')), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refreshBackups: function() {
|
||||
var self = this;
|
||||
callListBackups().then(function(data) {
|
||||
self.backups = (data || {}).backups || [];
|
||||
var container = document.getElementById('backups-list');
|
||||
if (container) dom.content(container, self.renderBackupsList());
|
||||
});
|
||||
},
|
||||
|
||||
refreshLogs: function() {
|
||||
callLogs().then(function(data) {
|
||||
var logs = (data.logs || '').replace(/\|/g, '\n');
|
||||
var el = document.getElementById('logs-content');
|
||||
if (el) {
|
||||
el.textContent = logs || '(No logs available)';
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Refresh
|
||||
// ========================================================================
|
||||
|
||||
refresh: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callListBackups().catch(function() { return { backups: [] }; })
|
||||
]).then(function(data) {
|
||||
self.status = data[0] || {};
|
||||
self.backups = (data[1] || {}).backups || [];
|
||||
|
||||
// Update tab content
|
||||
var tabContent = document.getElementById('tab-content');
|
||||
if (tabContent) {
|
||||
dom.content(tabContent, self.renderTabContent());
|
||||
}
|
||||
|
||||
// Refresh logs if on logs tab
|
||||
if (self.currentTab === 'logs') {
|
||||
self.refreshLogs();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -13,59 +13,128 @@ return view.extend({
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('nextcloud', _('Nextcloud Settings'),
|
||||
_('Configure Nextcloud settings. Changes require service restart to take effect.'));
|
||||
_('Configure Nextcloud LXC container settings. Changes require service restart to take effect.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'nextcloud', _('General Settings'));
|
||||
s.anonymous = true;
|
||||
// Main Settings
|
||||
s = m.section(form.NamedSection, 'main', 'nextcloud', _('General Settings'));
|
||||
s.anonymous = false;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'),
|
||||
_('Enable Nextcloud'));
|
||||
_('Enable Nextcloud service'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Web UI Port'),
|
||||
o = s.option(form.Value, 'http_port', _('HTTP Port'),
|
||||
_('Port for the Nextcloud web interface'));
|
||||
o.datatype = 'port';
|
||||
o.default = '80';
|
||||
o.placeholder = '80';
|
||||
o.default = '8080';
|
||||
o.placeholder = '8080';
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
||||
_('Path to store Nextcloud data'));
|
||||
_('Path to store Nextcloud data and backups'));
|
||||
o.default = '/srv/nextcloud';
|
||||
o.placeholder = '/srv/nextcloud';
|
||||
|
||||
o = s.option(form.Value, 'domain', _('Domain'),
|
||||
_('Primary domain name for Nextcloud'));
|
||||
o.default = 'cloud.local';
|
||||
o.placeholder = 'cloud.local';
|
||||
|
||||
o = s.option(form.Value, 'admin_user', _('Admin Username'),
|
||||
_('Administrator username for initial setup'));
|
||||
_('Administrator username'));
|
||||
o.default = 'admin';
|
||||
o.placeholder = 'admin';
|
||||
|
||||
o = s.option(form.Value, 'admin_password', _('Admin Password'),
|
||||
_('Administrator password for initial setup. Required for first install.'));
|
||||
_('Administrator password (only used during initial setup)'));
|
||||
o.password = true;
|
||||
o.placeholder = _('Enter password');
|
||||
|
||||
o = s.option(form.Value, 'trusted_domains', _('Trusted Domains'),
|
||||
_('Comma-separated list of trusted domains (e.g., cloud.example.com,192.168.1.1)'));
|
||||
o.default = 'cloud.local';
|
||||
o.placeholder = 'cloud.local';
|
||||
o = s.option(form.Value, 'memory_limit', _('PHP Memory Limit'),
|
||||
_('Memory limit for PHP (e.g., 512M, 1G, 2G)'));
|
||||
o.default = '1G';
|
||||
o.placeholder = '1G';
|
||||
|
||||
o = s.option(form.Value, 'timezone', _('Timezone'),
|
||||
_('Timezone for the container'));
|
||||
o.default = 'UTC';
|
||||
o.placeholder = 'UTC';
|
||||
o = s.option(form.Value, 'upload_max', _('Max Upload Size'),
|
||||
_('Maximum file upload size (e.g., 512M, 1G)'));
|
||||
o.default = '512M';
|
||||
o.placeholder = '512M';
|
||||
|
||||
o = s.option(form.Value, 'image', _('Docker Image'),
|
||||
_('Docker image to use'));
|
||||
o.default = 'nextcloud:latest';
|
||||
o.placeholder = 'nextcloud:latest';
|
||||
o = s.option(form.Value, 'trusted_proxies', _('Trusted Proxies'),
|
||||
_('IP addresses of trusted reverse proxies'));
|
||||
o.default = '127.0.0.1';
|
||||
o.placeholder = '127.0.0.1';
|
||||
|
||||
// Redis Cache Settings
|
||||
s = m.section(form.NamedSection, 'redis', 'cache', _('Redis Cache'));
|
||||
s.anonymous = false;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Redis'),
|
||||
_('Enable Redis caching for improved performance'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'memory', _('Redis Memory'),
|
||||
_('Maximum memory for Redis cache'));
|
||||
o.default = '128M';
|
||||
o.placeholder = '128M';
|
||||
|
||||
// SSL Settings
|
||||
s = m.section(form.NamedSection, 'ssl', 'haproxy', _('SSL / HAProxy'));
|
||||
s.anonymous = false;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable SSL'),
|
||||
_('Enable HTTPS via HAProxy'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'domain', _('SSL Domain'),
|
||||
_('Domain name for SSL certificate'));
|
||||
o.placeholder = 'cloud.example.com';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'acme', _('Auto SSL (ACME)'),
|
||||
_('Automatically obtain SSL certificate from Let\'s Encrypt'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.depends('enabled', '1');
|
||||
|
||||
// Backup Settings
|
||||
s = m.section(form.NamedSection, 'backup', 'backup', _('Backup Settings'));
|
||||
s.anonymous = false;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Auto Backup'),
|
||||
_('Enable automatic scheduled backups'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'schedule', _('Backup Schedule'),
|
||||
_('How often to create automatic backups'));
|
||||
o.value('daily', _('Daily'));
|
||||
o.value('weekly', _('Weekly'));
|
||||
o.value('hourly', _('Hourly'));
|
||||
o.default = 'daily';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'keep', _('Keep Backups'),
|
||||
_('Number of backups to retain'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '7';
|
||||
o.placeholder = '7';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'path', _('Backup Path'),
|
||||
_('Directory to store backups'));
|
||||
o.default = '/srv/nextcloud/backups';
|
||||
o.placeholder = '/srv/nextcloud/backups';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
return m.render().then(function(node) {
|
||||
return KissTheme.wrap(node, 'admin/secubox/services/nextcloud/settings');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,47 +1,62 @@
|
||||
#!/bin/sh
|
||||
# RPCD backend for Nextcloud LuCI app
|
||||
# RPCD backend for Nextcloud LuCI app (LXC version)
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
CONFIG="nextcloud"
|
||||
CONTAINER="secbx-nextcloud"
|
||||
LXC_NAME="nextcloud"
|
||||
LXC_ROOTFS="/srv/lxc/nextcloud/rootfs"
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
# Check if LXC container is running
|
||||
lxc_running() {
|
||||
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||
}
|
||||
|
||||
# Check if container is installed
|
||||
lxc_installed() {
|
||||
[ -d "$LXC_ROOTFS" ] && [ -f "/srv/lxc/$LXC_NAME/config" ]
|
||||
}
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local admin_user=$(uci_get admin_user)
|
||||
local trusted_domains=$(uci_get trusted_domains)
|
||||
local image=$(uci_get image)
|
||||
local enabled=$(uci_get main.enabled)
|
||||
local http_port=$(uci_get main.http_port)
|
||||
local data_path=$(uci_get main.data_path)
|
||||
local domain=$(uci_get main.domain)
|
||||
local ssl_enabled=$(uci_get ssl.enabled)
|
||||
local ssl_domain=$(uci_get ssl.domain)
|
||||
|
||||
# Check if Docker is available
|
||||
local docker_available=0
|
||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
||||
|
||||
# Check if container is running
|
||||
# Container status
|
||||
local running=0
|
||||
local container_status="stopped"
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
if docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then
|
||||
running=1
|
||||
container_status="running"
|
||||
elif docker ps -a --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then
|
||||
container_status="stopped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if installed (image exists)
|
||||
local installed=0
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
docker images --format "{{.Repository}}" 2>/dev/null | grep -q "nextcloud" && installed=1
|
||||
local version=""
|
||||
local user_count=0
|
||||
local disk_used="0"
|
||||
|
||||
lxc_installed && installed=1
|
||||
lxc_running && running=1
|
||||
|
||||
# Get Nextcloud info if running
|
||||
if [ "$running" = "1" ]; then
|
||||
version=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ -V" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "")
|
||||
user_count=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ user:list --output=json" 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l || echo 0)
|
||||
fi
|
||||
|
||||
# Check web UI accessibility
|
||||
# Get disk usage
|
||||
if [ -d "${data_path:-/srv/nextcloud}/data" ]; then
|
||||
disk_used=$(du -sh "${data_path:-/srv/nextcloud}/data" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
fi
|
||||
|
||||
# Get LAN IP
|
||||
local lan_ip
|
||||
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1")
|
||||
|
||||
# Check web accessibility
|
||||
local web_accessible=0
|
||||
if [ "$running" = "1" ]; then
|
||||
wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port:-80}/" 2>/dev/null && web_accessible=1
|
||||
wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${http_port:-8080}/status.php" 2>/dev/null && web_accessible=1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
@ -49,40 +64,50 @@ get_status() {
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||
"container_status": "$container_status",
|
||||
"port": ${port:-80},
|
||||
"version": "$version",
|
||||
"http_port": ${http_port:-8080},
|
||||
"data_path": "${data_path:-/srv/nextcloud}",
|
||||
"admin_user": "${admin_user:-admin}",
|
||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
||||
"image": "${image:-nextcloud:latest}",
|
||||
"domain": "${domain:-cloud.local}",
|
||||
"user_count": $user_count,
|
||||
"disk_used": "$disk_used",
|
||||
"web_url": "http://${lan_ip}:${http_port:-8080}",
|
||||
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
|
||||
"web_url": "http://192.168.255.1:${port:-80}"
|
||||
"ssl_enabled": $([ "$ssl_enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"ssl_domain": "${ssl_domain:-}",
|
||||
"container_name": "$LXC_NAME"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get configuration
|
||||
get_config() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local admin_user=$(uci_get admin_user)
|
||||
local admin_password=$(uci_get admin_password)
|
||||
local trusted_domains=$(uci_get trusted_domains)
|
||||
local timezone=$(uci_get timezone)
|
||||
local image=$(uci_get image)
|
||||
local enabled=$(uci_get main.enabled)
|
||||
local http_port=$(uci_get main.http_port)
|
||||
local data_path=$(uci_get main.data_path)
|
||||
local domain=$(uci_get main.domain)
|
||||
local admin_user=$(uci_get main.admin_user)
|
||||
local memory_limit=$(uci_get main.memory_limit)
|
||||
local upload_max=$(uci_get main.upload_max)
|
||||
local redis_enabled=$(uci_get redis.enabled)
|
||||
local ssl_enabled=$(uci_get ssl.enabled)
|
||||
local ssl_domain=$(uci_get ssl.domain)
|
||||
local backup_enabled=$(uci_get backup.enabled)
|
||||
local backup_keep=$(uci_get backup.keep)
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": "${enabled:-0}",
|
||||
"port": "${port:-80}",
|
||||
"http_port": "${http_port:-8080}",
|
||||
"data_path": "${data_path:-/srv/nextcloud}",
|
||||
"domain": "${domain:-cloud.local}",
|
||||
"admin_user": "${admin_user:-admin}",
|
||||
"admin_password": "${admin_password:-}",
|
||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
||||
"timezone": "${timezone:-UTC}",
|
||||
"image": "${image:-nextcloud:latest}"
|
||||
"memory_limit": "${memory_limit:-1G}",
|
||||
"upload_max": "${upload_max:-512M}",
|
||||
"redis_enabled": "${redis_enabled:-1}",
|
||||
"ssl_enabled": "${ssl_enabled:-0}",
|
||||
"ssl_domain": "${ssl_domain:-}",
|
||||
"backup_enabled": "${backup_enabled:-1}",
|
||||
"backup_keep": "${backup_keep:-7}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
@ -92,19 +117,19 @@ save_config() {
|
||||
local input
|
||||
read -r input
|
||||
|
||||
local port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
|
||||
local http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null)
|
||||
local data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
|
||||
local domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||
local admin_user=$(echo "$input" | jsonfilter -e '@.admin_user' 2>/dev/null)
|
||||
local admin_password=$(echo "$input" | jsonfilter -e '@.admin_password' 2>/dev/null)
|
||||
local trusted_domains=$(echo "$input" | jsonfilter -e '@.trusted_domains' 2>/dev/null)
|
||||
local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null)
|
||||
local memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
|
||||
local upload_max=$(echo "$input" | jsonfilter -e '@.upload_max' 2>/dev/null)
|
||||
|
||||
[ -n "$port" ] && uci_set port "$port"
|
||||
[ -n "$data_path" ] && uci_set data_path "$data_path"
|
||||
[ -n "$admin_user" ] && uci_set admin_user "$admin_user"
|
||||
[ -n "$admin_password" ] && uci_set admin_password "$admin_password"
|
||||
[ -n "$trusted_domains" ] && uci_set trusted_domains "$trusted_domains"
|
||||
[ -n "$timezone" ] && uci_set timezone "$timezone"
|
||||
[ -n "$http_port" ] && uci_set main.http_port "$http_port"
|
||||
[ -n "$data_path" ] && uci_set main.data_path "$data_path"
|
||||
[ -n "$domain" ] && uci_set main.domain "$domain"
|
||||
[ -n "$admin_user" ] && uci_set main.admin_user "$admin_user"
|
||||
[ -n "$memory_limit" ] && uci_set main.memory_limit "$memory_limit"
|
||||
[ -n "$upload_max" ] && uci_set main.upload_max "$upload_max"
|
||||
|
||||
echo '{"success": true}'
|
||||
}
|
||||
@ -122,8 +147,9 @@ do_install() {
|
||||
# Start service
|
||||
do_start() {
|
||||
if [ -x /etc/init.d/nextcloud ]; then
|
||||
uci_set main.enabled '1'
|
||||
/etc/init.d/nextcloud start >/dev/null 2>&1
|
||||
uci_set enabled '1'
|
||||
sleep 2
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
@ -160,6 +186,99 @@ do_update() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Create backup
|
||||
do_backup() {
|
||||
local input
|
||||
read -r input
|
||||
local name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
local result
|
||||
if [ -n "$name" ]; then
|
||||
result=$(nextcloudctl backup "$name" 2>&1)
|
||||
else
|
||||
result=$(nextcloudctl backup 2>&1)
|
||||
fi
|
||||
echo "{\"success\": true, \"backup_name\": \"$result\"}"
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Restore backup
|
||||
do_restore() {
|
||||
local input
|
||||
read -r input
|
||||
local name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
echo '{"success": false, "error": "No backup name specified"}'
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
nextcloudctl restore "$name" >/tmp/nextcloud-restore.log 2>&1 &
|
||||
echo '{"success": true, "message": "Restore started in background"}'
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# List backups
|
||||
list_backups() {
|
||||
local data_path=$(uci_get main.data_path)
|
||||
local backup_dir="${data_path:-/srv/nextcloud}/backups"
|
||||
local backups="[]"
|
||||
|
||||
if [ -d "$backup_dir" ]; then
|
||||
backups=$(ls -1 "$backup_dir"/*-db.sql 2>/dev/null | while read f; do
|
||||
local name=$(basename "$f" -db.sql)
|
||||
local data_file="$backup_dir/$name-data.tar.gz"
|
||||
local size="N/A"
|
||||
local timestamp=0
|
||||
|
||||
if [ -f "$data_file" ]; then
|
||||
size=$(ls -lh "$data_file" | awk '{print $5}')
|
||||
fi
|
||||
timestamp=$(stat -c %Y "$f" 2>/dev/null || echo 0)
|
||||
|
||||
printf '{"name":"%s","size":"%s","timestamp":%d},' "$name" "$size" "$timestamp"
|
||||
done | sed 's/,$//' | sed 's/^/[/;s/$/]/')
|
||||
fi
|
||||
|
||||
[ -z "$backups" ] && backups="[]"
|
||||
echo "{\"backups\": $backups}"
|
||||
}
|
||||
|
||||
# Enable SSL
|
||||
do_ssl_enable() {
|
||||
local input
|
||||
read -r input
|
||||
local domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
echo '{"success": false, "error": "No domain specified"}'
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
nextcloudctl ssl-enable "$domain" >/tmp/nextcloud-ssl.log 2>&1
|
||||
echo '{"success": true, "message": "SSL enabled for '$domain'"}'
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Disable SSL
|
||||
do_ssl_disable() {
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
nextcloudctl ssl-disable >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Run OCC command
|
||||
do_occ() {
|
||||
local input
|
||||
@ -173,7 +292,8 @@ do_occ() {
|
||||
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
local output=$(nextcloudctl occ $cmd 2>&1)
|
||||
echo "{\"success\": true, \"output\": \"$(echo "$output" | sed 's/"/\\"/g' | tr '\n' ' ')\"}"
|
||||
local escaped=$(echo "$output" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||
echo "{\"success\": true, \"output\": \"$escaped\"}"
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
@ -181,9 +301,10 @@ do_occ() {
|
||||
|
||||
# Get logs
|
||||
get_logs() {
|
||||
local lines=50
|
||||
local lines=100
|
||||
local log_content=""
|
||||
|
||||
# Check install log
|
||||
if [ -f /tmp/nextcloud-install.log ]; then
|
||||
log_content=$(tail -n $lines /tmp/nextcloud-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||
fi
|
||||
@ -197,12 +318,17 @@ list_methods() {
|
||||
{
|
||||
"status": {},
|
||||
"get_config": {},
|
||||
"save_config": {"port": "string", "data_path": "string", "admin_user": "string", "admin_password": "string", "trusted_domains": "string", "timezone": "string"},
|
||||
"save_config": {"http_port": "string", "data_path": "string", "domain": "string", "admin_user": "string", "memory_limit": "string", "upload_max": "string"},
|
||||
"install": {},
|
||||
"start": {},
|
||||
"stop": {},
|
||||
"restart": {},
|
||||
"update": {},
|
||||
"backup": {"name": "string"},
|
||||
"restore": {"name": "string"},
|
||||
"list_backups": {},
|
||||
"ssl_enable": {"domain": "string"},
|
||||
"ssl_disable": {},
|
||||
"occ": {"command": "string"},
|
||||
"logs": {}
|
||||
}
|
||||
@ -216,17 +342,22 @@ case "$1" in
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
get_config) get_config ;;
|
||||
save_config) save_config ;;
|
||||
install) do_install ;;
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
update) do_update ;;
|
||||
occ) do_occ ;;
|
||||
logs) get_logs ;;
|
||||
*) echo '{"error": "Unknown method"}' ;;
|
||||
status) get_status ;;
|
||||
get_config) get_config ;;
|
||||
save_config) save_config ;;
|
||||
install) do_install ;;
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
update) do_update ;;
|
||||
backup) do_backup ;;
|
||||
restore) do_restore ;;
|
||||
list_backups) list_backups ;;
|
||||
ssl_enable) do_ssl_enable ;;
|
||||
ssl_disable) do_ssl_disable ;;
|
||||
occ) do_occ ;;
|
||||
logs) get_logs ;;
|
||||
*) echo '{"error": "Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"admin/services/nextcloud": {
|
||||
"admin/secubox/services/nextcloud": {
|
||||
"title": "Nextcloud",
|
||||
"order": 55,
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
@ -9,7 +9,7 @@
|
||||
"acl": ["luci-app-nextcloud"]
|
||||
}
|
||||
},
|
||||
"admin/services/nextcloud/overview": {
|
||||
"admin/secubox/services/nextcloud/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
@ -17,9 +17,9 @@
|
||||
"path": "nextcloud/overview"
|
||||
}
|
||||
},
|
||||
"admin/services/nextcloud/settings": {
|
||||
"admin/secubox/services/nextcloud/settings": {
|
||||
"title": "Settings",
|
||||
"order": 90,
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "nextcloud/settings"
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
{
|
||||
"luci-app-nextcloud": {
|
||||
"description": "Grant access to Nextcloud",
|
||||
"description": "Grant access to Nextcloud LXC app",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.nextcloud": ["status", "get_config", "logs"]
|
||||
"luci.nextcloud": ["status", "get_config", "list_backups", "logs"]
|
||||
},
|
||||
"uci": ["nextcloud"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.nextcloud": ["install", "start", "stop", "restart", "update", "save_config", "occ"]
|
||||
"luci.nextcloud": [
|
||||
"install",
|
||||
"start",
|
||||
"stop",
|
||||
"restart",
|
||||
"update",
|
||||
"save_config",
|
||||
"backup",
|
||||
"restore",
|
||||
"ssl_enable",
|
||||
"ssl_disable",
|
||||
"occ"
|
||||
]
|
||||
},
|
||||
"uci": ["nextcloud"]
|
||||
}
|
||||
|
||||
@ -14,14 +14,14 @@ define Package/secubox-app-nextcloud
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
SUBMENU:=SecuBox Apps
|
||||
TITLE:=SecuBox Nextcloud docker app
|
||||
DEPENDS:=dockerd +docker +containerd
|
||||
TITLE:=SecuBox Nextcloud LXC app
|
||||
DEPENDS:=+lxc +lxc-common +tar +wget-ssl +jsonfilter +openssl-util +unzip +xz
|
||||
endef
|
||||
|
||||
define Package/secubox-app-nextcloud/description
|
||||
Installer, configuration, and service manager for running Nextcloud
|
||||
inside Docker on SecuBox-powered OpenWrt systems. Self-hosted file
|
||||
sync and share with calendar, contacts, and collaboration.
|
||||
Nextcloud file sync and collaboration platform running in a Debian-based
|
||||
LXC container with MariaDB, Redis, and Nginx. Features HAProxy SSL
|
||||
integration, automated backups, and KISS LuCI dashboard.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-nextcloud/conffiles
|
||||
|
||||
@ -1,9 +1,31 @@
|
||||
config nextcloud 'main'
|
||||
option enabled '0'
|
||||
option image 'nextcloud:latest'
|
||||
option data_path '/srv/nextcloud'
|
||||
option port '80'
|
||||
option http_port '8080'
|
||||
option domain 'cloud.local'
|
||||
option admin_user 'admin'
|
||||
option admin_password ''
|
||||
option trusted_domains 'cloud.local'
|
||||
option timezone 'UTC'
|
||||
option memory_limit '1G'
|
||||
option upload_max '512M'
|
||||
option trusted_proxies '127.0.0.1'
|
||||
|
||||
config database 'db'
|
||||
option type 'mariadb'
|
||||
option name 'nextcloud'
|
||||
option user 'nextcloud'
|
||||
option password ''
|
||||
|
||||
config cache 'redis'
|
||||
option enabled '1'
|
||||
option memory '128M'
|
||||
|
||||
config haproxy 'ssl'
|
||||
option enabled '0'
|
||||
option domain ''
|
||||
option acme '1'
|
||||
|
||||
config backup 'backup'
|
||||
option enabled '1'
|
||||
option schedule 'daily'
|
||||
option keep '7'
|
||||
option path '/srv/nextcloud/backups'
|
||||
|
||||
@ -1,23 +1,40 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SecuBox Nextcloud LXC Service
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
START=95
|
||||
START=90
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
SERVICE_BIN="/usr/sbin/nextcloudctl"
|
||||
PROG=/usr/sbin/nextcloudctl
|
||||
CONFIG=nextcloud
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$SERVICE_BIN" service-run
|
||||
procd_set_param respawn 2000 5 5
|
||||
procd_set_param command "$PROG" service-run
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/nextcloud.pid
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
"$SERVICE_BIN" service-stop >/dev/null 2>&1
|
||||
"$PROG" service-stop
|
||||
}
|
||||
|
||||
restart_service() {
|
||||
reload_service() {
|
||||
stop_service
|
||||
start_service
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIG"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user