secubox-openwrt/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/overview.js
CyberMind-FR 4a0ab9530f feat(mesh): Yggdrasil extended peer discovery + bugfixes
## New Features
- secubox-app-yggdrasil-discovery: Mesh peer discovery via gossip protocol
  - yggctl CLI: status, self, peers, announce, discover, bootstrap
  - Auto-peering with trust verification (master-link fingerprint)
  - Daemon for periodic announcements

## Bug Fixes
- tor-shield: Fix opkg downloads failing when Tor active
  - DNS over Tor disabled by default
  - Auto-exclude public DNS servers from iptables rules
  - Excluded domains bypass list (openwrt.org, pool.ntp.org, etc.)

- haproxy: Fix portal 503 "End of Internet" error
  - Corrected malformed vhost backend configuration
  - Regenerated HAProxy config from UCI

- luci-app-nextcloud: Fix users list showing empty
  - RPC expect clause was extracting array, render expected object

## Updated
- Bonus feed: All IPKs rebuilt
- Documentation: HISTORY.md, WIP.md, TODO.md updated

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 17:32:41 +01:00

980 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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: {}
});
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: {}
});
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
});