'use strict'; 'require view'; 'require dom'; 'require ui'; 'require rpc'; 'require poll'; 'require secubox/kiss-theme'; // ============================================================================ // RPC Declarations // ============================================================================ 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: {} }); var callGetStorage = rpc.declare({ object: 'luci.nextcloud', method: 'get_storage', expect: {} }); var callDeleteBackup = rpc.declare({ object: 'luci.nextcloud', method: 'delete_backup', params: ['name'], expect: {} }); var callUninstall = rpc.declare({ object: 'luci.nextcloud', method: 'uninstall', expect: {} }); var callListUsers = rpc.declare({ object: 'luci.nextcloud', method: 'list_users', expect: { users: [] } }); var callResetPassword = rpc.declare({ object: 'luci.nextcloud', method: 'reset_password', params: ['uid', 'password'], 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({ status: {}, config: {}, backups: [], users: [], storage: {}, currentTab: 'overview', load: function() { return Promise.all([ callStatus(), callGetConfig(), callListBackups().catch(function() { return { backups: [] }; }), callListUsers().catch(function() { return { users: [] }; }), callGetStorage().catch(function() { return {}; }) ]); }, render: function(data) { var self = this; this.status = data[0] || {}; this.config = data[1] || {}; this.backups = (data[2] || {}).backups || []; this.users = (data[3] || {}).users || []; this.storage = data[4] || {}; // Not installed - show install view if (!this.status.installed) { return this.renderInstallView(); } // Tab navigation var tabs = [ { id: 'overview', label: 'Overview', icon: 'đŸŽ›ī¸' }, { id: 'users', label: 'Users', icon: 'đŸ‘Ĩ' }, { id: 'storage', label: 'Storage', 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); }); }, renderTabContent: function() { switch (this.currentTab) { case 'users': return this.renderUsersTab(); case 'storage': return this.renderStorageTab(); case 'backups': return this.renderBackupsTab(); case 'ssl': return this.renderSSLTab(); case 'logs': return this.renderLogsTab(); default: return this.renderOverviewTab(); } }, // ======================================================================== // Install View // ======================================================================== 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)) ]); }, // ======================================================================== // Users Tab // ======================================================================== renderUsersTab: function() { var self = this; return E('div', {}, [ // Users List KissTheme.card([ E('span', {}, 'đŸ‘Ĩ Nextcloud Users'), E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.users.length + ' users') ], E('div', { 'id': 'users-list' }, this.renderUsersList())) ]); }, renderUsersList: function() { var self = this; if (!this.users.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 users found'), E('div', { 'style': 'font-size:12px;margin-top:8px;' }, 'Container may not be running') ]); } return E('table', { 'class': 'kiss-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'User ID'), E('th', {}, 'Display Name'), E('th', { 'style': 'width:120px;' }, 'Actions') ])), E('tbody', {}, this.users.map(function(u) { return E('tr', {}, [ E('td', { 'style': 'font-family:monospace;' }, u.uid || u), E('td', {}, u.displayname || '-'), E('td', {}, [ E('button', { 'class': 'kiss-btn', 'style': 'padding:4px 10px;font-size:11px;', 'title': 'Reset Password', 'data-uid': u.uid || u, 'click': function(ev) { self.showResetPasswordModal(ev.currentTarget.dataset.uid); } }, '🔑') ]) ]); })) ]); }, showResetPasswordModal: function(uid) { var self = this; var passwordInput, confirmInput; ui.showModal('Reset Password - ' + uid, [ E('div', { 'style': 'padding:16px;' }, [ E('p', { 'style': 'margin-bottom:16px;color:var(--kiss-muted);' }, 'Enter new password for user: ' + uid), E('div', { 'style': 'margin-bottom:12px;' }, [ E('label', { 'style': 'display:block;font-size:12px;color:var(--kiss-muted);margin-bottom:4px;' }, 'New Password'), passwordInput = E('input', { 'type': 'password', 'style': 'width:100%;padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);' }) ]), E('div', { 'style': 'margin-bottom:16px;' }, [ E('label', { 'style': 'display:block;font-size:12px;color:var(--kiss-muted);margin-bottom:4px;' }, 'Confirm Password'), confirmInput = E('input', { 'type': 'password', 'style': 'width:100%;padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);' }) ]), E('div', { 'style': 'display:flex;gap:8px;justify-content:flex-end;' }, [ E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), E('button', { 'class': 'kiss-btn kiss-btn-green', 'click': function() { var password = passwordInput.value; var confirm = confirmInput.value; if (!password) { ui.addNotification(null, E('p', 'Password required'), 'error'); return; } if (password !== confirm) { ui.addNotification(null, E('p', 'Passwords do not match'), 'error'); return; } ui.hideModal(); self.handleResetPassword(uid, password); } }, 'Reset Password') ]) ]) ]); }, handleResetPassword: function(uid, password) { var self = this; ui.showModal('Resetting Password', [ E('p', { 'class': 'spinning' }, 'Resetting password for ' + uid + '...') ]); callResetPassword(uid, password).then(function(r) { ui.hideModal(); if (r.success) { ui.addNotification(null, E('p', 'Password reset for ' + uid), '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'); }); }, // ======================================================================== // Storage Tab // ======================================================================== renderStorageTab: function() { var self = this; var st = this.storage; return E('div', {}, [ // Storage Stats E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom:24px;' }, [ KissTheme.stat(st.data_size || '0', 'User Data', 'var(--kiss-cyan)'), KissTheme.stat(st.backup_size || '0', 'Backups', 'var(--kiss-purple)'), KissTheme.stat(st.disk_free || '0', 'Disk Free', 'var(--kiss-green)'), KissTheme.stat((st.disk_used_percent || 0) + '%', 'Disk Used', st.disk_used_percent > 80 ? 'var(--kiss-red)' : 'var(--kiss-blue)') ]), // Disk Usage Bar KissTheme.card([ E('span', {}, 'đŸ’ŋ Disk Usage') ], E('div', {}, [ E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;' }, [ E('span', {}, 'Used: ' + (st.disk_used_percent || 0) + '%'), E('span', { 'style': 'color:var(--kiss-muted);' }, st.disk_free + ' free of ' + st.disk_total) ]), E('div', { 'style': 'height:24px;background:var(--kiss-bg2);border-radius:12px;overflow:hidden;' }, [ E('div', { 'style': 'height:100%;width:' + (st.disk_used_percent || 0) + '%;background:linear-gradient(90deg, var(--kiss-cyan), var(--kiss-blue));transition:width 0.3s;' }) ]) ])), // Storage Details KissTheme.card([ E('span', {}, '📊 Storage Breakdown') ], E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [ this.storageRow('📁 User Data', st.data_size || '0', 'var(--kiss-cyan)'), this.storageRow('💾 Backups', st.backup_size || '0', 'var(--kiss-purple)'), this.storageRow('đŸ“Ļ Total Nextcloud', st.total_size || '0', 'var(--kiss-blue)') ])), // Data Path Info KissTheme.card([ E('span', {}, 'â„šī¸ Data Location') ], E('div', { 'style': 'font-family:monospace;padding:12px;background:var(--kiss-bg2);border-radius:6px;' }, this.status.data_path || '/srv/nextcloud' )) ]); }, storageRow: function(label, size, color) { return E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;padding:10px;background:var(--kiss-bg2);border-radius:6px;' }, [ E('span', { 'style': 'display:flex;align-items:center;gap:8px;' }, label), E('span', { 'style': 'font-weight:600;color:' + color + ';' }, size) ]); }, // ======================================================================== // 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', { 'style': 'width:150px;' }, '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', { 'style': 'display:flex;gap:6px;' }, [ 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'), E('button', { 'class': 'kiss-btn kiss-btn-red', 'style': 'padding:4px 10px;font-size:11px;', 'data-name': b.name, 'click': function(ev) { self.handleDeleteBackup(ev.currentTarget.dataset.name); } }, 'đŸ—‘ī¸') ]) ]); })) ]); }, // ======================================================================== // 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(); 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'); } }); }, handleDeleteBackup: function(name) { var self = this; if (!confirm('Delete backup "' + name + '"? This cannot be undone.')) { return; } ui.showModal('Deleting Backup', [ E('p', { 'class': 'spinning' }, 'Deleting ' + name + '...') ]); callDeleteBackup(name).then(function(r) { ui.hideModal(); if (r.success) { ui.addNotification(null, E('p', 'Backup deleted: ' + name), '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'); }); }, 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: [] }; }), callListUsers().catch(function() { return { users: [] }; }), callGetStorage().catch(function() { return {}; }) ]).then(function(data) { self.status = data[0] || {}; self.backups = (data[1] || {}).backups || []; self.users = (data[2] || {}).users || []; self.storage = data[3] || {}; // 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, handleSave: null, handleReset: null });