Fixed "No related RPC reply" errors across all admin views by wrapping
ALL RPC calls in L.resolveDefault() with appropriate fallback values.
This allows the frontend to load gracefully even when the backend RPCD
methods are not yet deployed, showing empty data instead of crashing.
Changes:
- health.js: Wrapped getHealth() → L.resolveDefault(getHealth(), {})
- logs.js: Wrapped getLogs() → L.resolveDefault(getLogs(), { logs: '' })
- settings.js: Wrapped getApps() and getModules() with fallbacks
- apps.js: Wrapped getApps() and getModules() (checkUpdates already wrapped)
- dashboard.js: Wrapped all 4 RPC calls (getApps, getModules, getHealth, getAlerts)
- Incremented PKG_RELEASE: 6 → 7
- Updated DEPLOY_UPDATES.md with v1.0.0-7 details
All admin pages now load successfully regardless of backend deployment status.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
356 lines
10 KiB
JavaScript
356 lines
10 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require secubox-admin.api as API';
|
|
'require secubox-admin.components as Components';
|
|
'require ui';
|
|
'require form';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
L.resolveDefault(API.getApps(), { apps: [] }),
|
|
L.resolveDefault(API.getModules(), { modules: {} }),
|
|
L.resolveDefault(API.checkUpdates(), {})
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var apps = data[0].apps || [];
|
|
var modules = data[1].modules || {};
|
|
var updateInfo = data[2] || {};
|
|
var self = this;
|
|
|
|
// Create updates lookup map
|
|
var updatesMap = {};
|
|
if (updateInfo.updates) {
|
|
updateInfo.updates.forEach(function(update) {
|
|
updatesMap[update.app_id] = update;
|
|
});
|
|
}
|
|
|
|
var container = E('div', { 'class': 'secubox-apps-manager' }, [
|
|
E('link', { 'rel': 'stylesheet',
|
|
'href': L.resource('secubox-admin/common.css') }),
|
|
E('link', { 'rel': 'stylesheet',
|
|
'href': L.resource('secubox-admin/admin.css') }),
|
|
|
|
E('h2', {}, 'Apps Manager'),
|
|
E('p', {}, 'Browse and manage SecuBox applications from the catalog'),
|
|
|
|
// Filters
|
|
E('div', { 'class': 'app-filters' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'class': 'search-box',
|
|
'placeholder': 'Search apps...',
|
|
'keyup': function(ev) {
|
|
self.filterApps(ev.target.value);
|
|
}
|
|
}),
|
|
E('select', {
|
|
'class': 'category-filter',
|
|
'change': function(ev) {
|
|
self.filterByCategory(ev.target.value);
|
|
}
|
|
}, [
|
|
E('option', { 'value': '' }, 'All Categories'),
|
|
E('option', { 'value': 'security' }, 'Security'),
|
|
E('option', { 'value': 'network' }, 'Network'),
|
|
E('option', { 'value': 'hosting' }, 'Hosting'),
|
|
E('option', { 'value': 'productivity' }, 'Productivity')
|
|
]),
|
|
E('select', {
|
|
'class': 'status-filter',
|
|
'change': function(ev) {
|
|
self.filterByStatus(ev.target.value);
|
|
}
|
|
}, [
|
|
E('option', { 'value': '' }, 'All Apps'),
|
|
E('option', { 'value': 'update-available' }, 'Updates Available'),
|
|
E('option', { 'value': 'installed' }, 'Installed'),
|
|
E('option', { 'value': 'not-installed' }, 'Not Installed')
|
|
])
|
|
]),
|
|
|
|
// Apps grid
|
|
E('div', { 'class': 'apps-grid', 'id': 'apps-grid' },
|
|
apps.map(function(app) {
|
|
var status = API.getAppStatus(app, modules);
|
|
var updateAvailable = updatesMap[app.id];
|
|
return self.renderAppCard(app, status, updateAvailable);
|
|
})
|
|
)
|
|
]);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderAppCard: function(app, status, updateInfo) {
|
|
var self = this;
|
|
var hasUpdate = updateInfo && updateInfo.update_available;
|
|
|
|
var cardClasses = 'app-card';
|
|
if (status.installed) cardClasses += ' installed';
|
|
if (hasUpdate) cardClasses += ' has-update';
|
|
|
|
return E('div', {
|
|
'class': cardClasses,
|
|
'data-category': app.category,
|
|
'data-update-status': hasUpdate ? 'update-available' : '',
|
|
'data-install-status': status.installed ? 'installed' : 'not-installed'
|
|
}, [
|
|
E('div', { 'class': 'app-icon' }, app.icon || '📦'),
|
|
E('div', { 'class': 'app-info' }, [
|
|
E('div', { 'class': 'app-title-row' }, [
|
|
E('h3', {}, app.name),
|
|
hasUpdate ? E('span', { 'class': 'badge badge-warning update-badge' }, 'Update') : null
|
|
]),
|
|
E('p', { 'class': 'app-description' }, app.description),
|
|
E('div', { 'class': 'app-meta' }, [
|
|
E('span', { 'class': 'app-category' }, app.category),
|
|
E('span', {
|
|
'class': 'app-version' + (hasUpdate ? ' version-outdated' : ''),
|
|
'title': hasUpdate ?
|
|
'Installed: ' + updateInfo.installed_version + ' → Available: ' + updateInfo.catalog_version :
|
|
''
|
|
}, 'v' + (app.pkg_version || app.version || '1.0')),
|
|
Components.renderStatusBadge(status.status)
|
|
])
|
|
]),
|
|
E('div', { 'class': 'app-actions' },
|
|
status.installed ? [
|
|
hasUpdate ? E('button', {
|
|
'class': 'btn btn-sm btn-warning',
|
|
'click': function() { self.updateApp(app, updateInfo); }
|
|
}, 'Update') : null,
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-secondary',
|
|
'click': function() { self.viewChangelog(app); }
|
|
}, 'Changelog'),
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-primary',
|
|
'click': function() { self.configureApp(app); }
|
|
}, 'Configure'),
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-danger',
|
|
'click': function() { self.removeApp(app); }
|
|
}, 'Remove')
|
|
] : [
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-secondary',
|
|
'click': function() { self.viewChangelog(app); }
|
|
}, 'Changelog'),
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-success',
|
|
'click': function() { self.installApp(app); }
|
|
}, 'Install')
|
|
]
|
|
)
|
|
]);
|
|
},
|
|
|
|
installApp: function(app) {
|
|
var self = this;
|
|
ui.showModal('Install ' + app.name, [
|
|
E('p', {}, 'Are you sure you want to install ' + app.name + '?'),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'btn btn-primary',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
ui.showModal('Installing...', [
|
|
Components.renderLoader('Installing ' + app.name + '...')
|
|
]);
|
|
API.installApp(app.id).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', app.name + ' installed successfully'), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed to install ' + app.name), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Install')
|
|
])
|
|
]);
|
|
},
|
|
|
|
removeApp: function(app) {
|
|
var self = this;
|
|
ui.showModal('Remove ' + app.name, [
|
|
E('p', {}, 'Are you sure you want to remove ' + app.name + '?'),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'btn btn-danger',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
ui.showModal('Removing...', [
|
|
Components.renderLoader('Removing ' + app.name + '...')
|
|
]);
|
|
API.removeApp(app.id).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', app.name + ' removed successfully'), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed to remove ' + app.name), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Remove')
|
|
])
|
|
]);
|
|
},
|
|
|
|
configureApp: function(app) {
|
|
window.location = L.url('admin/secubox/admin/settings');
|
|
},
|
|
|
|
filterApps: function(query) {
|
|
var cards = document.querySelectorAll('.app-card');
|
|
query = query.toLowerCase();
|
|
cards.forEach(function(card) {
|
|
var name = card.querySelector('h3').textContent.toLowerCase();
|
|
var desc = card.querySelector('.app-description').textContent.toLowerCase();
|
|
if (name.includes(query) || desc.includes(query)) {
|
|
card.style.display = '';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
},
|
|
|
|
filterByCategory: function(category) {
|
|
var cards = document.querySelectorAll('.app-card');
|
|
cards.forEach(function(card) {
|
|
if (!category || card.dataset.category === category) {
|
|
card.style.display = '';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
},
|
|
|
|
filterByStatus: function(status) {
|
|
var cards = document.querySelectorAll('.app-card');
|
|
cards.forEach(function(card) {
|
|
if (!status) {
|
|
card.style.display = '';
|
|
} else if (status === 'update-available') {
|
|
card.style.display = card.dataset.updateStatus === 'update-available' ? '' : 'none';
|
|
} else if (status === 'installed') {
|
|
card.style.display = card.dataset.installStatus === 'installed' ? '' : 'none';
|
|
} else if (status === 'not-installed') {
|
|
card.style.display = card.dataset.installStatus === 'not-installed' ? '' : 'none';
|
|
}
|
|
});
|
|
},
|
|
|
|
updateApp: function(app, updateInfo) {
|
|
var self = this;
|
|
ui.showModal('Update ' + app.name, [
|
|
E('p', {}, 'Update ' + app.name + ' from v' +
|
|
updateInfo.installed_version + ' to v' + updateInfo.catalog_version + '?'),
|
|
updateInfo.changelog ? E('div', { 'class': 'update-changelog' }, [
|
|
E('h4', {}, 'What\'s New:'),
|
|
E('div', {},
|
|
Array.isArray(updateInfo.changelog) ?
|
|
E('ul', {},
|
|
updateInfo.changelog.map(function(item) {
|
|
return E('li', {}, item);
|
|
})
|
|
) :
|
|
E('p', {}, updateInfo.changelog)
|
|
)
|
|
]) : null,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'btn btn-warning',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
ui.showModal('Updating...', [
|
|
Components.renderLoader('Updating ' + app.name + '...')
|
|
]);
|
|
API.installApp(app.id).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', app.name + ' updated successfully'), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed to update ' + app.name), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Update')
|
|
])
|
|
]);
|
|
},
|
|
|
|
viewChangelog: function(app) {
|
|
ui.showModal('Changelog: ' + app.name, [
|
|
E('p', { 'class': 'spinning' }, 'Loading changelog...')
|
|
]);
|
|
|
|
API.getChangelog(app.id, null, null).then(function(changelog) {
|
|
var content = E('div', { 'class': 'changelog-viewer' });
|
|
|
|
if (changelog && changelog.changelog) {
|
|
var versions = Object.keys(changelog.changelog);
|
|
versions.forEach(function(version) {
|
|
var versionData = changelog.changelog[version];
|
|
content.appendChild(E('div', { 'class': 'changelog-version' }, [
|
|
E('h4', {}, 'Version ' + version),
|
|
versionData.date ? E('p', { 'class': 'changelog-date' }, versionData.date) : null,
|
|
E('ul', {},
|
|
(versionData.changes || []).map(function(change) {
|
|
return E('li', {}, change);
|
|
})
|
|
)
|
|
]));
|
|
});
|
|
} else if (typeof changelog === 'string') {
|
|
content.appendChild(E('pre', {}, changelog));
|
|
} else {
|
|
content.appendChild(E('p', {}, 'No changelog available'));
|
|
}
|
|
|
|
ui.showModal('Changelog: ' + app.name, [
|
|
content,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Close')
|
|
])
|
|
]);
|
|
}).catch(function(err) {
|
|
ui.showModal('Changelog: ' + app.name, [
|
|
E('p', {}, 'Failed to load changelog: ' + err.message),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Close')
|
|
])
|
|
]);
|
|
});
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|