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
|
- MetaBlogizer: HAProxy vhost lookup for automatic subdomain detection
|
||||||
- Added more icons for new service types
|
- Added more icons for new service types
|
||||||
- **Result**: 67 services with proper subdomain URLs
|
- **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
|
- Gossip-based exposure config sync via secubox-p2p
|
||||||
- Created `luci-app-vortex-dns` dashboard
|
- 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)
|
- **HAProxy & Mitmproxy WAF Fixes** — DONE (2026-02-15)
|
||||||
- Fixed HAProxy reload: copy config to `/etc/haproxy/` before signal
|
- Fixed HAProxy reload: copy config to `/etc/haproxy/` before signal
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI support for Nextcloud
|
LUCI_TITLE:=LuCI support for Nextcloud LXC
|
||||||
LUCI_DEPENDS:=+luci-base
|
LUCI_DEPENDS:=+luci-base +luci-lib-secubox +secubox-app-nextcloud
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
PKG_NAME:=luci-app-nextcloud
|
PKG_NAME:=luci-app-nextcloud
|
||||||
|
|||||||
@ -1,214 +1,722 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
|
'require dom';
|
||||||
'require ui';
|
'require ui';
|
||||||
'require rpc';
|
'require rpc';
|
||||||
'require poll';
|
'require poll';
|
||||||
'require secubox/kiss-theme';
|
'require secubox/kiss-theme';
|
||||||
|
|
||||||
var callStatus = rpc.declare({ object: 'luci.nextcloud', method: 'status', expect: {} });
|
// ============================================================================
|
||||||
var callInstall = rpc.declare({ object: 'luci.nextcloud', method: 'install', expect: {} });
|
// RPC Declarations
|
||||||
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 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({
|
return view.extend({
|
||||||
pollActive: true,
|
status: {},
|
||||||
|
config: {},
|
||||||
|
backups: [],
|
||||||
|
currentTab: 'overview',
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return callStatus();
|
return Promise.all([
|
||||||
},
|
callStatus(),
|
||||||
|
callGetConfig(),
|
||||||
startPolling: function() {
|
callListBackups().catch(function() { return { backups: [] }; })
|
||||||
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...'))
|
|
||||||
]);
|
]);
|
||||||
callInstall().then(function(r) {
|
},
|
||||||
ui.hideModal();
|
|
||||||
if (r.success) {
|
render: function(data) {
|
||||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
var self = this;
|
||||||
self.startPolling();
|
this.status = data[0] || {};
|
||||||
} else {
|
this.config = data[1] || {};
|
||||||
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown error')), 'error');
|
this.backups = (data[2] || {}).backups || [];
|
||||||
|
|
||||||
|
// Not installed - show install view
|
||||||
|
if (!this.status.installed) {
|
||||||
|
return this.renderInstallView();
|
||||||
}
|
}
|
||||||
}).catch(function(e) {
|
|
||||||
ui.hideModal();
|
// Tab navigation
|
||||||
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
|
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() {
|
renderTabContent: function() {
|
||||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Nextcloud...'))]);
|
switch (this.currentTab) {
|
||||||
callStart().then(function(r) {
|
case 'backups': return this.renderBackupsTab();
|
||||||
ui.hideModal();
|
case 'ssl': return this.renderSSLTab();
|
||||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud started')));
|
case 'logs': return this.renderLogsTab();
|
||||||
else ui.addNotification(null, E('p', _('Failed to start')), 'error');
|
default: return this.renderOverviewTab();
|
||||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleStop: function() {
|
// ========================================================================
|
||||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping Nextcloud...'))]);
|
// Install View
|
||||||
callStop().then(function(r) {
|
// ========================================================================
|
||||||
|
|
||||||
|
renderInstallView: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
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.'),
|
||||||
|
|
||||||
|
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('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 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'))
|
||||||
|
]);
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// SSL Tab
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
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))),
|
||||||
|
|
||||||
|
// 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': '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.')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
callInstall().then(function(r) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud stopped')));
|
if (r.success) {
|
||||||
else ui.addNotification(null, E('p', _('Failed to stop')), 'error');
|
ui.addNotification(null, E('p', r.message || 'Installation started'), 'info');
|
||||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
} 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() {
|
handleRestart: function() {
|
||||||
ui.showModal(_('Restarting...'), [E('p', { 'class': 'spinning' }, _('Restarting Nextcloud...'))]);
|
var self = this;
|
||||||
|
ui.showModal('Restarting...', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Restarting Nextcloud...')
|
||||||
|
]);
|
||||||
|
|
||||||
callRestart().then(function(r) {
|
callRestart().then(function(r) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud restarted')));
|
if (r.success) {
|
||||||
else ui.addNotification(null, E('p', _('Failed to restart')), 'error');
|
ui.addNotification(null, E('p', 'Nextcloud restarted'), 'info');
|
||||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
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');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(status) {
|
handleUpdate: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
ui.showModal('Updating Nextcloud', [
|
||||||
if (!document.getElementById('nc-styles')) {
|
E('div', { 'style': 'text-align:center;padding:20px;' }, [
|
||||||
var s = document.createElement('style');
|
E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, '⬆️'),
|
||||||
s.id = 'nc-styles';
|
E('p', { 'style': 'margin-top:16px;' }, 'Updating Nextcloud...'),
|
||||||
s.textContent = css;
|
E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take a few minutes.')
|
||||||
document.head.appendChild(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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', { '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'))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Installed view
|
|
||||||
this.startPolling();
|
|
||||||
|
|
||||||
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'))
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
|
|
||||||
// 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' }, [
|
|
||||||
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'))
|
|
||||||
])
|
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return KissTheme.wrap(content, 'admin/secubox/services/nextcloud');
|
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,
|
handleSaveApply: null,
|
||||||
|
|||||||
@ -13,59 +13,128 @@ return view.extend({
|
|||||||
var m, s, o;
|
var m, s, o;
|
||||||
|
|
||||||
m = new form.Map('nextcloud', _('Nextcloud Settings'),
|
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'));
|
// Main Settings
|
||||||
s.anonymous = true;
|
s = m.section(form.NamedSection, 'main', 'nextcloud', _('General Settings'));
|
||||||
|
s.anonymous = false;
|
||||||
s.addremove = false;
|
s.addremove = false;
|
||||||
|
|
||||||
o = s.option(form.Flag, 'enabled', _('Enabled'),
|
o = s.option(form.Flag, 'enabled', _('Enabled'),
|
||||||
_('Enable Nextcloud'));
|
_('Enable Nextcloud service'));
|
||||||
o.default = '0';
|
o.default = '0';
|
||||||
o.rmempty = false;
|
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'));
|
_('Port for the Nextcloud web interface'));
|
||||||
o.datatype = 'port';
|
o.datatype = 'port';
|
||||||
o.default = '80';
|
o.default = '8080';
|
||||||
o.placeholder = '80';
|
o.placeholder = '8080';
|
||||||
|
|
||||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
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.default = '/srv/nextcloud';
|
||||||
o.placeholder = '/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'),
|
o = s.option(form.Value, 'admin_user', _('Admin Username'),
|
||||||
_('Administrator username for initial setup'));
|
_('Administrator username'));
|
||||||
o.default = 'admin';
|
o.default = 'admin';
|
||||||
o.placeholder = 'admin';
|
o.placeholder = 'admin';
|
||||||
|
|
||||||
o = s.option(form.Value, 'admin_password', _('Admin Password'),
|
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.password = true;
|
||||||
o.placeholder = _('Enter password');
|
o.placeholder = _('Enter password');
|
||||||
|
|
||||||
o = s.option(form.Value, 'trusted_domains', _('Trusted Domains'),
|
o = s.option(form.Value, 'memory_limit', _('PHP Memory Limit'),
|
||||||
_('Comma-separated list of trusted domains (e.g., cloud.example.com,192.168.1.1)'));
|
_('Memory limit for PHP (e.g., 512M, 1G, 2G)'));
|
||||||
o.default = 'cloud.local';
|
o.default = '1G';
|
||||||
o.placeholder = 'cloud.local';
|
o.placeholder = '1G';
|
||||||
|
|
||||||
o = s.option(form.Value, 'timezone', _('Timezone'),
|
o = s.option(form.Value, 'upload_max', _('Max Upload Size'),
|
||||||
_('Timezone for the container'));
|
_('Maximum file upload size (e.g., 512M, 1G)'));
|
||||||
o.default = 'UTC';
|
o.default = '512M';
|
||||||
o.placeholder = 'UTC';
|
o.placeholder = '512M';
|
||||||
|
|
||||||
o = s.option(form.Value, 'image', _('Docker Image'),
|
o = s.option(form.Value, 'trusted_proxies', _('Trusted Proxies'),
|
||||||
_('Docker image to use'));
|
_('IP addresses of trusted reverse proxies'));
|
||||||
o.default = 'nextcloud:latest';
|
o.default = '127.0.0.1';
|
||||||
o.placeholder = 'nextcloud:latest';
|
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 m.render().then(function(node) {
|
||||||
return KissTheme.wrap(node, 'admin/secubox/services/nextcloud/settings');
|
return KissTheme.wrap(node, 'admin/secubox/services/nextcloud/settings');
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
handleSaveApply: null,
|
|
||||||
handleSave: null,
|
|
||||||
handleReset: null
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,47 +1,62 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# RPCD backend for Nextcloud LuCI app
|
# RPCD backend for Nextcloud LuCI app (LXC version)
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
|
||||||
CONFIG="nextcloud"
|
CONFIG="nextcloud"
|
||||||
CONTAINER="secbx-nextcloud"
|
LXC_NAME="nextcloud"
|
||||||
|
LXC_ROOTFS="/srv/lxc/nextcloud/rootfs"
|
||||||
|
|
||||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
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 service status
|
||||||
get_status() {
|
get_status() {
|
||||||
local enabled=$(uci_get enabled)
|
local enabled=$(uci_get main.enabled)
|
||||||
local port=$(uci_get port)
|
local http_port=$(uci_get main.http_port)
|
||||||
local data_path=$(uci_get data_path)
|
local data_path=$(uci_get main.data_path)
|
||||||
local admin_user=$(uci_get admin_user)
|
local domain=$(uci_get main.domain)
|
||||||
local trusted_domains=$(uci_get trusted_domains)
|
local ssl_enabled=$(uci_get ssl.enabled)
|
||||||
local image=$(uci_get image)
|
local ssl_domain=$(uci_get ssl.domain)
|
||||||
|
|
||||||
# Check if Docker is available
|
# Container status
|
||||||
local docker_available=0
|
|
||||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
|
||||||
|
|
||||||
# Check if container is running
|
|
||||||
local running=0
|
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
|
local installed=0
|
||||||
if [ "$docker_available" = "1" ]; then
|
local version=""
|
||||||
docker images --format "{{.Repository}}" 2>/dev/null | grep -q "nextcloud" && installed=1
|
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
|
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
|
local web_accessible=0
|
||||||
if [ "$running" = "1" ]; then
|
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
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@ -49,40 +64,50 @@ get_status() {
|
|||||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
"version": "$version",
|
||||||
"container_status": "$container_status",
|
"http_port": ${http_port:-8080},
|
||||||
"port": ${port:-80},
|
|
||||||
"data_path": "${data_path:-/srv/nextcloud}",
|
"data_path": "${data_path:-/srv/nextcloud}",
|
||||||
"admin_user": "${admin_user:-admin}",
|
"domain": "${domain:-cloud.local}",
|
||||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
"user_count": $user_count,
|
||||||
"image": "${image:-nextcloud:latest}",
|
"disk_used": "$disk_used",
|
||||||
|
"web_url": "http://${lan_ip}:${http_port:-8080}",
|
||||||
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
|
"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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get configuration
|
# Get configuration
|
||||||
get_config() {
|
get_config() {
|
||||||
local enabled=$(uci_get enabled)
|
local enabled=$(uci_get main.enabled)
|
||||||
local port=$(uci_get port)
|
local http_port=$(uci_get main.http_port)
|
||||||
local data_path=$(uci_get data_path)
|
local data_path=$(uci_get main.data_path)
|
||||||
local admin_user=$(uci_get admin_user)
|
local domain=$(uci_get main.domain)
|
||||||
local admin_password=$(uci_get admin_password)
|
local admin_user=$(uci_get main.admin_user)
|
||||||
local trusted_domains=$(uci_get trusted_domains)
|
local memory_limit=$(uci_get main.memory_limit)
|
||||||
local timezone=$(uci_get timezone)
|
local upload_max=$(uci_get main.upload_max)
|
||||||
local image=$(uci_get image)
|
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
|
cat <<EOF
|
||||||
{
|
{
|
||||||
"enabled": "${enabled:-0}",
|
"enabled": "${enabled:-0}",
|
||||||
"port": "${port:-80}",
|
"http_port": "${http_port:-8080}",
|
||||||
"data_path": "${data_path:-/srv/nextcloud}",
|
"data_path": "${data_path:-/srv/nextcloud}",
|
||||||
|
"domain": "${domain:-cloud.local}",
|
||||||
"admin_user": "${admin_user:-admin}",
|
"admin_user": "${admin_user:-admin}",
|
||||||
"admin_password": "${admin_password:-}",
|
"memory_limit": "${memory_limit:-1G}",
|
||||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
"upload_max": "${upload_max:-512M}",
|
||||||
"timezone": "${timezone:-UTC}",
|
"redis_enabled": "${redis_enabled:-1}",
|
||||||
"image": "${image:-nextcloud:latest}"
|
"ssl_enabled": "${ssl_enabled:-0}",
|
||||||
|
"ssl_domain": "${ssl_domain:-}",
|
||||||
|
"backup_enabled": "${backup_enabled:-1}",
|
||||||
|
"backup_keep": "${backup_keep:-7}"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -92,19 +117,19 @@ save_config() {
|
|||||||
local input
|
local input
|
||||||
read -r 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 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_user=$(echo "$input" | jsonfilter -e '@.admin_user' 2>/dev/null)
|
||||||
local admin_password=$(echo "$input" | jsonfilter -e '@.admin_password' 2>/dev/null)
|
local memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
|
||||||
local trusted_domains=$(echo "$input" | jsonfilter -e '@.trusted_domains' 2>/dev/null)
|
local upload_max=$(echo "$input" | jsonfilter -e '@.upload_max' 2>/dev/null)
|
||||||
local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null)
|
|
||||||
|
|
||||||
[ -n "$port" ] && uci_set port "$port"
|
[ -n "$http_port" ] && uci_set main.http_port "$http_port"
|
||||||
[ -n "$data_path" ] && uci_set data_path "$data_path"
|
[ -n "$data_path" ] && uci_set main.data_path "$data_path"
|
||||||
[ -n "$admin_user" ] && uci_set admin_user "$admin_user"
|
[ -n "$domain" ] && uci_set main.domain "$domain"
|
||||||
[ -n "$admin_password" ] && uci_set admin_password "$admin_password"
|
[ -n "$admin_user" ] && uci_set main.admin_user "$admin_user"
|
||||||
[ -n "$trusted_domains" ] && uci_set trusted_domains "$trusted_domains"
|
[ -n "$memory_limit" ] && uci_set main.memory_limit "$memory_limit"
|
||||||
[ -n "$timezone" ] && uci_set timezone "$timezone"
|
[ -n "$upload_max" ] && uci_set main.upload_max "$upload_max"
|
||||||
|
|
||||||
echo '{"success": true}'
|
echo '{"success": true}'
|
||||||
}
|
}
|
||||||
@ -122,8 +147,9 @@ do_install() {
|
|||||||
# Start service
|
# Start service
|
||||||
do_start() {
|
do_start() {
|
||||||
if [ -x /etc/init.d/nextcloud ]; then
|
if [ -x /etc/init.d/nextcloud ]; then
|
||||||
|
uci_set main.enabled '1'
|
||||||
/etc/init.d/nextcloud start >/dev/null 2>&1
|
/etc/init.d/nextcloud start >/dev/null 2>&1
|
||||||
uci_set enabled '1'
|
sleep 2
|
||||||
echo '{"success": true}'
|
echo '{"success": true}'
|
||||||
else
|
else
|
||||||
echo '{"success": false, "error": "Service not installed"}'
|
echo '{"success": false, "error": "Service not installed"}'
|
||||||
@ -160,6 +186,99 @@ do_update() {
|
|||||||
fi
|
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
|
# Run OCC command
|
||||||
do_occ() {
|
do_occ() {
|
||||||
local input
|
local input
|
||||||
@ -173,7 +292,8 @@ do_occ() {
|
|||||||
|
|
||||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||||
local output=$(nextcloudctl occ $cmd 2>&1)
|
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
|
else
|
||||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||||
fi
|
fi
|
||||||
@ -181,9 +301,10 @@ do_occ() {
|
|||||||
|
|
||||||
# Get logs
|
# Get logs
|
||||||
get_logs() {
|
get_logs() {
|
||||||
local lines=50
|
local lines=100
|
||||||
local log_content=""
|
local log_content=""
|
||||||
|
|
||||||
|
# Check install log
|
||||||
if [ -f /tmp/nextcloud-install.log ]; then
|
if [ -f /tmp/nextcloud-install.log ]; then
|
||||||
log_content=$(tail -n $lines /tmp/nextcloud-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
log_content=$(tail -n $lines /tmp/nextcloud-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||||
fi
|
fi
|
||||||
@ -197,12 +318,17 @@ list_methods() {
|
|||||||
{
|
{
|
||||||
"status": {},
|
"status": {},
|
||||||
"get_config": {},
|
"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": {},
|
"install": {},
|
||||||
"start": {},
|
"start": {},
|
||||||
"stop": {},
|
"stop": {},
|
||||||
"restart": {},
|
"restart": {},
|
||||||
"update": {},
|
"update": {},
|
||||||
|
"backup": {"name": "string"},
|
||||||
|
"restore": {"name": "string"},
|
||||||
|
"list_backups": {},
|
||||||
|
"ssl_enable": {"domain": "string"},
|
||||||
|
"ssl_disable": {},
|
||||||
"occ": {"command": "string"},
|
"occ": {"command": "string"},
|
||||||
"logs": {}
|
"logs": {}
|
||||||
}
|
}
|
||||||
@ -224,6 +350,11 @@ case "$1" in
|
|||||||
stop) do_stop ;;
|
stop) do_stop ;;
|
||||||
restart) do_restart ;;
|
restart) do_restart ;;
|
||||||
update) do_update ;;
|
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 ;;
|
occ) do_occ ;;
|
||||||
logs) get_logs ;;
|
logs) get_logs ;;
|
||||||
*) echo '{"error": "Unknown method"}' ;;
|
*) echo '{"error": "Unknown method"}' ;;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"admin/services/nextcloud": {
|
"admin/secubox/services/nextcloud": {
|
||||||
"title": "Nextcloud",
|
"title": "Nextcloud",
|
||||||
"order": 55,
|
"order": 30,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "firstchild"
|
"type": "firstchild"
|
||||||
},
|
},
|
||||||
@ -9,7 +9,7 @@
|
|||||||
"acl": ["luci-app-nextcloud"]
|
"acl": ["luci-app-nextcloud"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/services/nextcloud/overview": {
|
"admin/secubox/services/nextcloud/overview": {
|
||||||
"title": "Overview",
|
"title": "Overview",
|
||||||
"order": 10,
|
"order": 10,
|
||||||
"action": {
|
"action": {
|
||||||
@ -17,9 +17,9 @@
|
|||||||
"path": "nextcloud/overview"
|
"path": "nextcloud/overview"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/services/nextcloud/settings": {
|
"admin/secubox/services/nextcloud/settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"order": 90,
|
"order": 20,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"path": "nextcloud/settings"
|
"path": "nextcloud/settings"
|
||||||
|
|||||||
@ -1,15 +1,27 @@
|
|||||||
{
|
{
|
||||||
"luci-app-nextcloud": {
|
"luci-app-nextcloud": {
|
||||||
"description": "Grant access to Nextcloud",
|
"description": "Grant access to Nextcloud LXC app",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.nextcloud": ["status", "get_config", "logs"]
|
"luci.nextcloud": ["status", "get_config", "list_backups", "logs"]
|
||||||
},
|
},
|
||||||
"uci": ["nextcloud"]
|
"uci": ["nextcloud"]
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
"ubus": {
|
"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"]
|
"uci": ["nextcloud"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,14 +14,14 @@ define Package/secubox-app-nextcloud
|
|||||||
CATEGORY:=Utilities
|
CATEGORY:=Utilities
|
||||||
PKGARCH:=all
|
PKGARCH:=all
|
||||||
SUBMENU:=SecuBox Apps
|
SUBMENU:=SecuBox Apps
|
||||||
TITLE:=SecuBox Nextcloud docker app
|
TITLE:=SecuBox Nextcloud LXC app
|
||||||
DEPENDS:=dockerd +docker +containerd
|
DEPENDS:=+lxc +lxc-common +tar +wget-ssl +jsonfilter +openssl-util +unzip +xz
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/secubox-app-nextcloud/description
|
define Package/secubox-app-nextcloud/description
|
||||||
Installer, configuration, and service manager for running Nextcloud
|
Nextcloud file sync and collaboration platform running in a Debian-based
|
||||||
inside Docker on SecuBox-powered OpenWrt systems. Self-hosted file
|
LXC container with MariaDB, Redis, and Nginx. Features HAProxy SSL
|
||||||
sync and share with calendar, contacts, and collaboration.
|
integration, automated backups, and KISS LuCI dashboard.
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/secubox-app-nextcloud/conffiles
|
define Package/secubox-app-nextcloud/conffiles
|
||||||
|
|||||||
@ -1,9 +1,31 @@
|
|||||||
config nextcloud 'main'
|
config nextcloud 'main'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
option image 'nextcloud:latest'
|
|
||||||
option data_path '/srv/nextcloud'
|
option data_path '/srv/nextcloud'
|
||||||
option port '80'
|
option http_port '8080'
|
||||||
|
option domain 'cloud.local'
|
||||||
option admin_user 'admin'
|
option admin_user 'admin'
|
||||||
option admin_password ''
|
option admin_password ''
|
||||||
option trusted_domains 'cloud.local'
|
option memory_limit '1G'
|
||||||
option timezone 'UTC'
|
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
|
#!/bin/sh /etc/rc.common
|
||||||
|
# SecuBox Nextcloud LXC Service
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
|
||||||
START=95
|
START=90
|
||||||
STOP=10
|
STOP=10
|
||||||
USE_PROCD=1
|
USE_PROCD=1
|
||||||
|
|
||||||
SERVICE_BIN="/usr/sbin/nextcloudctl"
|
PROG=/usr/sbin/nextcloudctl
|
||||||
|
CONFIG=nextcloud
|
||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
procd_open_instance
|
procd_open_instance
|
||||||
procd_set_param command "$SERVICE_BIN" service-run
|
procd_set_param command "$PROG" service-run
|
||||||
procd_set_param respawn 2000 5 5
|
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
|
procd_close_instance
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_service() {
|
stop_service() {
|
||||||
"$SERVICE_BIN" service-stop >/dev/null 2>&1
|
"$PROG" service-stop
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_service() {
|
reload_service() {
|
||||||
stop_service
|
stop_service
|
||||||
start_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